JsonableBehavior.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. /**
  3. * Copyright 2011, PJ Hile (http://www.pjhile.com)
  4. *
  5. * Licensed under The MIT License
  6. * Redistributions of files must retain the above copyright notice.
  7. *
  8. * @version 0.1
  9. * @license http://opensource.org/licenses/mit-license.php MIT
  10. */
  11. App::uses('ModelBehavior', 'Model');
  12. App::uses('Hash', 'Utility');
  13. App::uses('String', 'Utility');
  14. /**
  15. * A behavior that will json_encode (and json_decode) fields if they contain an array or specific pattern.
  16. *
  17. * Requres: PHP 5 >= 5.2.0 or PECL json >= 1.2.0
  18. *
  19. * This is a port of the Serializeable behavior by Matsimitsu (http://www.matsimitsu.nl)
  20. * Modified by Mark Scherer (http://www.dereuromark.de)
  21. *
  22. * Now supports different input/output formats:
  23. * - "list" is useful as some kind of pseudo enums or simple lists
  24. * - "params" is useful for multiple key/value pairs
  25. * - can be used to create dynamic forms (and tables)
  26. * Also automatically cleans lists and works with custom separators etc
  27. *
  28. * Tip: If you have other behaviors that might modify the array data prior to saving, better use a higher priority:
  29. * public $actsAs = array('Tools.Jsonable' => array('priority' => 11, ...));
  30. * So that it is run last.
  31. *
  32. * @link http://www.dereuromark.de/2011/07/05/introducing-two-cakephp-behaviors/
  33. */
  34. class JsonableBehavior extends ModelBehavior {
  35. public $decoded = null;
  36. /**
  37. * //TODO: json input/ouput directly, clean
  38. * @var array
  39. */
  40. protected $_defaultConfig = array(
  41. 'fields' => array(), // empty => autodetect - only works with array!
  42. 'input' => 'array', // json, array, param, list (param/list only works with specific fields)
  43. 'output' => 'array', // json, array, param, list (param/list only works with specific fields)
  44. 'separator' => '|', // only for param or list
  45. 'keyValueSeparator' => ':', // only for param
  46. 'leftBound' => '{', // only for list
  47. 'rightBound' => '}', // only for list
  48. 'clean' => true, // only for param or list (autoclean values on insert)
  49. 'sort' => false, // only for list
  50. 'unique' => true, // only for list (autoclean values on insert),
  51. 'map' => array(), // map on a different DB field
  52. 'encodeParams' => array( // params for json_encode
  53. 'options' => 0,
  54. 'depth' => 512,
  55. ),
  56. 'decodeParams' => array( // params for json_decode
  57. 'assoc' => false, // useful when working with multidimensional arrays
  58. 'depth' => 512,
  59. 'options' => 0
  60. )
  61. );
  62. public function setup(Model $Model, $config = array()) {
  63. $this->settings[$Model->alias] = Hash::merge($this->_defaultConfig, $config);
  64. //extract($this->settings[$Model->alias]);
  65. if (!is_array($this->settings[$Model->alias]['fields'])) {
  66. $this->settings[$Model->alias]['fields'] = (array)$this->settings[$Model->alias]['fields'];
  67. }
  68. if (!is_array($this->settings[$Model->alias]['map'])) {
  69. $this->settings[$Model->alias]['map'] = (array)$this->settings[$Model->alias]['map'];
  70. }
  71. }
  72. /**
  73. * Decodes the fields
  74. *
  75. * @param Model $Model
  76. * @param array $results
  77. * @return array
  78. */
  79. public function afterFind(Model $Model, $results, $primary = false) {
  80. $results = $this->decodeItems($Model, $results);
  81. return $results;
  82. }
  83. /**
  84. * Decodes the fields of an array (if the value itself was encoded)
  85. *
  86. * @param array $arr
  87. * @return array
  88. */
  89. public function decodeItems(Model $Model, $arr) {
  90. foreach ($arr as $akey => $val) {
  91. if (!isset($val[$Model->alias])) {
  92. return $arr;
  93. }
  94. $fields = $this->settings[$Model->alias]['fields'];
  95. foreach ($val[$Model->alias] as $key => $v) {
  96. if (empty($fields) && !is_array($v) || !in_array($key, $fields)) {
  97. continue;
  98. }
  99. if ($this->isEncoded($Model, $v)) {
  100. if (!empty($this->settings[$Model->alias]['map'])) {
  101. $keys = array_keys($this->settings[$Model->alias]['fields'], $key);
  102. if (!empty($keys)) {
  103. $key = $this->settings[$Model->alias]['map'][array_shift($keys)];
  104. }
  105. }
  106. $arr[$akey][$Model->alias][$key] = $this->decoded;
  107. }
  108. }
  109. }
  110. return $arr;
  111. }
  112. /**
  113. * Saves all fields that do not belong to the current Model into 'with' helper model.
  114. *
  115. * @param Model $Model
  116. * @return bool Success
  117. */
  118. public function beforeSave(Model $Model, $options = array()) {
  119. $data = $Model->data[$Model->alias];
  120. $usedFields = $this->settings[$Model->alias]['fields'];
  121. $mappedFields = $this->settings[$Model->alias]['map'];
  122. if (empty($mappedFields)) {
  123. $mappedFields = $usedFields;
  124. }
  125. $fields = array();
  126. foreach ($mappedFields as $index => $map) {
  127. if (empty($map) || $map == $usedFields[$index]) {
  128. $fields[$usedFields[$index]] = $usedFields[$index];
  129. continue;
  130. }
  131. $fields[$map] = $usedFields[$index];
  132. }
  133. foreach ($data as $key => $val) {
  134. if (!empty($fields) && !array_key_exists($key, $fields)) {
  135. continue;
  136. }
  137. if (!empty($fields)) {
  138. $key = $fields[$key];
  139. }
  140. if (!empty($this->settings[$Model->alias]['fields']) || is_array($val)) {
  141. $Model->data[$Model->alias][$key] = $this->_encode($Model, $val);
  142. }
  143. }
  144. return true;
  145. }
  146. /**
  147. * JsonableBehavior::_encode()
  148. *
  149. * @param Model $Model
  150. * @param mixed $val
  151. * @return string
  152. */
  153. public function _encode(Model $Model, $val) {
  154. if (!empty($this->settings[$Model->alias]['fields'])) {
  155. if ($this->settings[$Model->alias]['input'] === 'param') {
  156. $val = $this->_fromParam($Model, $val);
  157. } elseif ($this->settings[$Model->alias]['input'] === 'list') {
  158. $val = $this->_fromList($Model, $val);
  159. if ($this->settings[$Model->alias]['unique']) {
  160. $val = array_unique($val);
  161. }
  162. if ($this->settings[$Model->alias]['sort']) {
  163. sort($val);
  164. }
  165. }
  166. }
  167. if (is_array($val)) {
  168. // $depth param added in php 5.5
  169. if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
  170. $val = json_encode($val, $this->settings[$Model->alias]['encodeParams']['options'], $this->settings[$Model->alias]['encodeParams']['depth']);
  171. } else {
  172. $val = json_encode($val, $this->settings[$Model->alias]['encodeParams']['options']);
  173. }
  174. }
  175. return $val;
  176. }
  177. /**
  178. * Fields are absolutely necessary to function properly!
  179. *
  180. * @param Model $Model
  181. * @param mixed $val
  182. * @return mixed
  183. */
  184. public function _decode(Model $Model, $val) {
  185. // $options param added in php 5.4
  186. if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
  187. $decoded = json_decode($val, $this->settings[$Model->alias]['decodeParams']['assoc'], $this->settings[$Model->alias]['decodeParams']['depth'], $this->settings[$Model->alias]['decodeParams']['options']);
  188. } else {
  189. $decoded = json_decode($val, $this->settings[$Model->alias]['decodeParams']['assoc'], $this->settings[$Model->alias]['decodeParams']['depth']);
  190. }
  191. if ($decoded === false) {
  192. return false;
  193. }
  194. $decoded = (array)$decoded;
  195. if ($this->settings[$Model->alias]['output'] === 'param') {
  196. $decoded = $this->_toParam($Model, $decoded);
  197. } elseif ($this->settings[$Model->alias]['output'] === 'list') {
  198. $decoded = $this->_toList($Model, $decoded);
  199. }
  200. return $decoded;
  201. }
  202. /**
  203. * array() => param1:value1|param2:value2|...
  204. */
  205. public function _toParam(Model $Model, $val) {
  206. $res = array();
  207. foreach ($val as $key => $v) {
  208. $res[] = $key . $this->settings[$Model->alias]['keyValueSeparator'] . $v;
  209. }
  210. return implode($this->settings[$Model->alias]['separator'], $res);
  211. }
  212. public function _fromParam(Model $Model, $val) {
  213. $leftBound = $this->settings[$Model->alias]['leftBound'];
  214. $rightBound = $this->settings[$Model->alias]['rightBound'];
  215. $separator = $this->settings[$Model->alias]['separator'];
  216. $res = array();
  217. $pieces = String::tokenize($val, $separator, $leftBound, $rightBound);
  218. foreach ($pieces as $piece) {
  219. $subpieces = String::tokenize($piece, $this->settings[$Model->alias]['keyValueSeparator'], $leftBound, $rightBound);
  220. if (count($subpieces) < 2) {
  221. continue;
  222. }
  223. $res[$subpieces[0]] = $subpieces[1];
  224. }
  225. return $res;
  226. }
  227. /**
  228. * array() => value1|value2|value3|...
  229. */
  230. public function _toList(Model $Model, $val) {
  231. return implode($this->settings[$Model->alias]['separator'], $val);
  232. }
  233. public function _fromList(Model $Model, $val) {
  234. extract($this->settings[$Model->alias]);
  235. return String::tokenize($val, $separator, $leftBound, $rightBound);
  236. }
  237. /**
  238. * Checks if string is encoded array/object
  239. *
  240. * @param string string to check
  241. * @return bool
  242. */
  243. public function isEncoded(Model $Model, $str) {
  244. $this->decoded = $this->_decode($Model, $str);
  245. if ($this->decoded !== false) {
  246. return true;
  247. }
  248. return false;
  249. }
  250. }