JsonableBehavior.php 8.1 KB

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