Set.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102
  1. <?php
  2. /**
  3. * Library of array functions for Cake.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Utility
  16. * @since CakePHP(tm) v 1.2.0
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. App::uses('String', 'Utility');
  20. App::uses('Hash', 'Utility');
  21. /**
  22. * Class used for manipulation of arrays.
  23. *
  24. * @package Cake.Utility
  25. */
  26. class Set {
  27. /**
  28. * This function can be thought of as a hybrid between PHP's array_merge and array_merge_recursive. The difference
  29. * to the two is that if an array key contains another array then the function behaves recursive (unlike array_merge)
  30. * but does not do if for keys containing strings (unlike array_merge_recursive).
  31. *
  32. * Since this method emulates `array_merge`, it will re-order numeric keys. When combined with out of
  33. * order numeric keys containing arrays, results can be lossy.
  34. *
  35. * Note: This function will work with an unlimited amount of arguments and typecasts non-array
  36. * parameters into arrays.
  37. *
  38. * @param array $arr1 Array to be merged
  39. * @param array $arr2 Array to merge with
  40. * @return array Merged array
  41. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::merge
  42. */
  43. public static function merge($arr1, $arr2 = null) {
  44. $args = func_get_args();
  45. return call_user_func_array('Hash::merge', $args);
  46. }
  47. /**
  48. * Filters empty elements out of a route array, excluding '0'.
  49. *
  50. * @param array $var Either an array to filter, or value when in callback
  51. * @return mixed Either filtered array, or true/false when in callback
  52. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::filter
  53. */
  54. public static function filter(array $var) {
  55. return Hash::filter($var);
  56. }
  57. /**
  58. * Pushes the differences in $array2 onto the end of $array
  59. *
  60. * @param mixed $array Original array
  61. * @param mixed $array2 Differences to push
  62. * @return array Combined array
  63. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::pushDiff
  64. */
  65. public static function pushDiff($array, $array2) {
  66. if (empty($array) && !empty($array2)) {
  67. return $array2;
  68. }
  69. if (!empty($array) && !empty($array2)) {
  70. foreach ($array2 as $key => $value) {
  71. if (!array_key_exists($key, $array)) {
  72. $array[$key] = $value;
  73. } else {
  74. if (is_array($value)) {
  75. $array[$key] = Set::pushDiff($array[$key], $array2[$key]);
  76. }
  77. }
  78. }
  79. }
  80. return $array;
  81. }
  82. /**
  83. * Maps the contents of the Set object to an object hierarchy.
  84. * Maintains numeric keys as arrays of objects
  85. *
  86. * @param string $class A class name of the type of object to map to
  87. * @param string $tmp A temporary class name used as $class if $class is an array
  88. * @return object Hierarchical object
  89. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::map
  90. */
  91. public static function map($class = 'stdClass', $tmp = 'stdClass') {
  92. if (is_array($class)) {
  93. $val = $class;
  94. $class = $tmp;
  95. }
  96. if (empty($val)) {
  97. return null;
  98. }
  99. return Set::_map($val, $class);
  100. }
  101. /**
  102. * Maps the given value as an object. If $value is an object,
  103. * it returns $value. Otherwise it maps $value as an object of
  104. * type $class, and if primary assign _name_ $key on first array.
  105. * If $value is not empty, it will be used to set properties of
  106. * returned object (recursively). If $key is numeric will maintain array
  107. * structure
  108. *
  109. * @param array $array Array to map
  110. * @param string $class Class name
  111. * @param boolean $primary whether to assign first array key as the _name_
  112. * @return mixed Mapped object
  113. */
  114. protected static function _map(&$array, $class, $primary = false) {
  115. if ($class === true) {
  116. $out = new stdClass;
  117. } else {
  118. $out = new $class;
  119. }
  120. if (is_array($array)) {
  121. $keys = array_keys($array);
  122. foreach ($array as $key => $value) {
  123. if ($keys[0] === $key && $class !== true) {
  124. $primary = true;
  125. }
  126. if (is_numeric($key)) {
  127. if (is_object($out)) {
  128. $out = get_object_vars($out);
  129. }
  130. $out[$key] = Set::_map($value, $class);
  131. if (is_object($out[$key])) {
  132. if ($primary !== true && is_array($value) && Set::countDim($value, true) === 2) {
  133. if (!isset($out[$key]->_name_)) {
  134. $out[$key]->_name_ = $primary;
  135. }
  136. }
  137. }
  138. } elseif (is_array($value)) {
  139. if ($primary === true) {
  140. // @codingStandardsIgnoreStart Legacy junk
  141. if (!isset($out->_name_)) {
  142. $out->_name_ = $key;
  143. }
  144. // @codingStandardsIgnoreEnd
  145. $primary = false;
  146. foreach ($value as $key2 => $value2) {
  147. $out->{$key2} = Set::_map($value2, true);
  148. }
  149. } else {
  150. if (!is_numeric($key)) {
  151. $out->{$key} = Set::_map($value, true, $key);
  152. if (is_object($out->{$key}) && !is_numeric($key)) {
  153. if (!isset($out->{$key}->_name_)) {
  154. $out->{$key}->_name_ = $key;
  155. }
  156. }
  157. } else {
  158. $out->{$key} = Set::_map($value, true);
  159. }
  160. }
  161. } else {
  162. $out->{$key} = $value;
  163. }
  164. }
  165. } else {
  166. $out = $array;
  167. }
  168. return $out;
  169. }
  170. /**
  171. * Checks to see if all the values in the array are numeric
  172. *
  173. * @param array $array The array to check. If null, the value of the current Set object
  174. * @return boolean true if values are numeric, false otherwise
  175. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::numeric
  176. */
  177. public static function numeric($array = null) {
  178. return Hash::numeric($array);
  179. }
  180. /**
  181. * Return a value from an array list if the key exists.
  182. *
  183. * If a comma separated $list is passed arrays are numeric with the key of the first being 0
  184. * $list = 'no, yes' would translate to $list = array(0 => 'no', 1 => 'yes');
  185. *
  186. * If an array is used, keys can be strings example: array('no' => 0, 'yes' => 1);
  187. *
  188. * $list defaults to 0 = no 1 = yes if param is not passed
  189. *
  190. * @param mixed $select Key in $list to return
  191. * @param mixed $list can be an array or a comma-separated list.
  192. * @return string the value of the array key or null if no match
  193. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::enum
  194. */
  195. public static function enum($select, $list = null) {
  196. if (empty($list)) {
  197. $list = array('no', 'yes');
  198. }
  199. $return = null;
  200. $list = Set::normalize($list, false);
  201. if (array_key_exists($select, $list)) {
  202. $return = $list[$select];
  203. }
  204. return $return;
  205. }
  206. /**
  207. * Returns a series of values extracted from an array, formatted in a format string.
  208. *
  209. * @param array $data Source array from which to extract the data
  210. * @param string $format Format string into which values will be inserted, see sprintf()
  211. * @param array $keys An array containing one or more Set::extract()-style key paths
  212. * @return array An array of strings extracted from $keys and formatted with $format
  213. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::format
  214. */
  215. public static function format($data, $format, $keys) {
  216. $extracted = array();
  217. $count = count($keys);
  218. if (!$count) {
  219. return;
  220. }
  221. for ($i = 0; $i < $count; $i++) {
  222. $extracted[] = Set::extract($data, $keys[$i]);
  223. }
  224. $out = array();
  225. $data = $extracted;
  226. $count = count($data[0]);
  227. if (preg_match_all('/\{([0-9]+)\}/msi', $format, $keys2) && isset($keys2[1])) {
  228. $keys = $keys2[1];
  229. $format = preg_split('/\{([0-9]+)\}/msi', $format);
  230. $count2 = count($format);
  231. for ($j = 0; $j < $count; $j++) {
  232. $formatted = '';
  233. for ($i = 0; $i <= $count2; $i++) {
  234. if (isset($format[$i])) {
  235. $formatted .= $format[$i];
  236. }
  237. if (isset($keys[$i]) && isset($data[$keys[$i]][$j])) {
  238. $formatted .= $data[$keys[$i]][$j];
  239. }
  240. }
  241. $out[] = $formatted;
  242. }
  243. } else {
  244. $count2 = count($data);
  245. for ($j = 0; $j < $count; $j++) {
  246. $args = array();
  247. for ($i = 0; $i < $count2; $i++) {
  248. if (array_key_exists($j, $data[$i])) {
  249. $args[] = $data[$i][$j];
  250. }
  251. }
  252. $out[] = vsprintf($format, $args);
  253. }
  254. }
  255. return $out;
  256. }
  257. /**
  258. * Implements partial support for XPath 2.0. If $path does not contain a '/' the call
  259. * is delegated to Set::classicExtract(). Also the $path and $data arguments are
  260. * reversible.
  261. *
  262. * #### Currently implemented selectors:
  263. *
  264. * - /User/id (similar to the classic {n}.User.id)
  265. * - /User[2]/name (selects the name of the second User)
  266. * - /User[id>2] (selects all Users with an id > 2)
  267. * - /User[id>2][<5] (selects all Users with an id > 2 but < 5)
  268. * - /Post/Comment[author_name=john]/../name (Selects the name of all Posts that have at least one Comment written by john)
  269. * - /Posts[name] (Selects all Posts that have a 'name' key)
  270. * - /Comment/.[1] (Selects the contents of the first comment)
  271. * - /Comment/.[:last] (Selects the last comment)
  272. * - /Comment/.[:first] (Selects the first comment)
  273. * - /Comment[text=/cakephp/i] (Selects the all comments that have a text matching the regex /cakephp/i)
  274. * - /Comment/@* (Selects the all key names of all comments)
  275. *
  276. * #### Other limitations:
  277. *
  278. * - Only absolute paths starting with a single '/' are supported right now
  279. *
  280. * **Warning**: Even so it has plenty of unit tests the XPath support has not gone through a lot of
  281. * real-world testing. Please report Bugs as you find them. Suggestions for additional features to
  282. * implement are also very welcome!
  283. *
  284. * @param string $path An absolute XPath 2.0 path
  285. * @param array $data An array of data to extract from
  286. * @param array $options Currently only supports 'flatten' which can be disabled for higher XPath-ness
  287. * @return array An array of matched items
  288. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::extract
  289. */
  290. public static function extract($path, $data = null, $options = array()) {
  291. if (is_string($data)) {
  292. $tmp = $data;
  293. $data = $path;
  294. $path = $tmp;
  295. }
  296. if (strpos($path, '/') === false) {
  297. return Set::classicExtract($data, $path);
  298. }
  299. if (empty($data)) {
  300. return array();
  301. }
  302. if ($path === '/') {
  303. return $data;
  304. }
  305. $contexts = $data;
  306. $options = array_merge(array('flatten' => true), $options);
  307. if (!isset($contexts[0])) {
  308. $current = current($data);
  309. if ((is_array($current) && count($data) < 1) || !is_array($current) || !Set::numeric(array_keys($data))) {
  310. $contexts = array($data);
  311. }
  312. }
  313. $tokens = array_slice(preg_split('/(?<!=|\\\\)\/(?![a-z-\s]*\])/', $path), 1);
  314. do {
  315. $token = array_shift($tokens);
  316. $conditions = false;
  317. if (preg_match_all('/\[([^=]+=\/[^\/]+\/|[^\]]+)\]/', $token, $m)) {
  318. $conditions = $m[1];
  319. $token = substr($token, 0, strpos($token, '['));
  320. }
  321. $matches = array();
  322. foreach ($contexts as $key => $context) {
  323. if (!isset($context['trace'])) {
  324. $context = array('trace' => array(null), 'item' => $context, 'key' => $key);
  325. }
  326. if ($token === '..') {
  327. if (count($context['trace']) == 1) {
  328. $context['trace'][] = $context['key'];
  329. }
  330. $parent = implode('/', $context['trace']) . '/.';
  331. $context['item'] = Set::extract($parent, $data);
  332. $context['key'] = array_pop($context['trace']);
  333. if (isset($context['trace'][1]) && $context['trace'][1] > 0) {
  334. $context['item'] = $context['item'][0];
  335. } elseif (!empty($context['item'][$key])) {
  336. $context['item'] = $context['item'][$key];
  337. } else {
  338. $context['item'] = array_shift($context['item']);
  339. }
  340. $matches[] = $context;
  341. continue;
  342. }
  343. if ($token === '@*' && is_array($context['item'])) {
  344. $matches[] = array(
  345. 'trace' => array_merge($context['trace'], (array)$key),
  346. 'key' => $key,
  347. 'item' => array_keys($context['item']),
  348. );
  349. } elseif (is_array($context['item'])
  350. && array_key_exists($token, $context['item'])
  351. && !(strval($key) === strval($token) && count($tokens) == 1 && $tokens[0] === '.')) {
  352. $items = $context['item'][$token];
  353. if (!is_array($items)) {
  354. $items = array($items);
  355. } elseif (!isset($items[0])) {
  356. $current = current($items);
  357. $currentKey = key($items);
  358. if (!is_array($current) || (is_array($current) && count($items) <= 1 && !is_numeric($currentKey))) {
  359. $items = array($items);
  360. }
  361. }
  362. foreach ($items as $key => $item) {
  363. $ctext = array($context['key']);
  364. if (!is_numeric($key)) {
  365. $ctext[] = $token;
  366. $tok = array_shift($tokens);
  367. if (isset($items[$tok])) {
  368. $ctext[] = $tok;
  369. $item = $items[$tok];
  370. $matches[] = array(
  371. 'trace' => array_merge($context['trace'], $ctext),
  372. 'key' => $tok,
  373. 'item' => $item,
  374. );
  375. break;
  376. } elseif ($tok !== null) {
  377. array_unshift($tokens, $tok);
  378. }
  379. } else {
  380. $key = $token;
  381. }
  382. $matches[] = array(
  383. 'trace' => array_merge($context['trace'], $ctext),
  384. 'key' => $key,
  385. 'item' => $item,
  386. );
  387. }
  388. } elseif ($key === $token || (ctype_digit($token) && $key == $token) || $token === '.') {
  389. $context['trace'][] = $key;
  390. $matches[] = array(
  391. 'trace' => $context['trace'],
  392. 'key' => $key,
  393. 'item' => $context['item'],
  394. );
  395. }
  396. }
  397. if ($conditions) {
  398. foreach ($conditions as $condition) {
  399. $filtered = array();
  400. $length = count($matches);
  401. foreach ($matches as $i => $match) {
  402. if (Set::matches(array($condition), $match['item'], $i + 1, $length)) {
  403. $filtered[$i] = $match;
  404. }
  405. }
  406. $matches = $filtered;
  407. }
  408. }
  409. $contexts = $matches;
  410. if (empty($tokens)) {
  411. break;
  412. }
  413. } while (1);
  414. $r = array();
  415. foreach ($matches as $match) {
  416. if ((!$options['flatten'] || is_array($match['item'])) && !is_int($match['key'])) {
  417. $r[] = array($match['key'] => $match['item']);
  418. } else {
  419. $r[] = $match['item'];
  420. }
  421. }
  422. return $r;
  423. }
  424. /**
  425. * This function can be used to see if a single item or a given xpath match certain conditions.
  426. *
  427. * @param mixed $conditions An array of condition strings or an XPath expression
  428. * @param array $data An array of data to execute the match on
  429. * @param integer $i Optional: The 'nth'-number of the item being matched.
  430. * @param integer $length
  431. * @return boolean
  432. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::matches
  433. */
  434. public static function matches($conditions, $data = array(), $i = null, $length = null) {
  435. if (empty($conditions)) {
  436. return true;
  437. }
  438. if (is_string($conditions)) {
  439. return !!Set::extract($conditions, $data);
  440. }
  441. foreach ($conditions as $condition) {
  442. if ($condition === ':last') {
  443. if ($i != $length) {
  444. return false;
  445. }
  446. continue;
  447. } elseif ($condition === ':first') {
  448. if ($i != 1) {
  449. return false;
  450. }
  451. continue;
  452. }
  453. if (!preg_match('/(.+?)([><!]?[=]|[><])(.*)/', $condition, $match)) {
  454. if (ctype_digit($condition)) {
  455. if ($i != $condition) {
  456. return false;
  457. }
  458. } elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) {
  459. return in_array($i, $matches[0]);
  460. } elseif (!array_key_exists($condition, $data)) {
  461. return false;
  462. }
  463. continue;
  464. }
  465. list(, $key, $op, $expected) = $match;
  466. if (!isset($data[$key])) {
  467. return false;
  468. }
  469. $val = $data[$key];
  470. if ($op === '=' && $expected && $expected{0} === '/') {
  471. return preg_match($expected, $val);
  472. }
  473. if ($op === '=' && $val != $expected) {
  474. return false;
  475. }
  476. if ($op === '!=' && $val == $expected) {
  477. return false;
  478. }
  479. if ($op === '>' && $val <= $expected) {
  480. return false;
  481. }
  482. if ($op === '<' && $val >= $expected) {
  483. return false;
  484. }
  485. if ($op === '<=' && $val > $expected) {
  486. return false;
  487. }
  488. if ($op === '>=' && $val < $expected) {
  489. return false;
  490. }
  491. }
  492. return true;
  493. }
  494. /**
  495. * Gets a value from an array or object that is contained in a given path using an array path syntax, i.e.:
  496. * "{n}.Person.{[a-z]+}" - Where "{n}" represents a numeric key, "Person" represents a string literal,
  497. * and "{[a-z]+}" (i.e. any string literal enclosed in brackets besides {n} and {s}) is interpreted as
  498. * a regular expression.
  499. *
  500. * @param array $data Array from where to extract
  501. * @param mixed $path As an array, or as a dot-separated string.
  502. * @return array Extracted data
  503. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::classicExtract
  504. */
  505. public static function classicExtract($data, $path = null) {
  506. if (empty($path)) {
  507. return $data;
  508. }
  509. if (is_object($data)) {
  510. if (!($data instanceof ArrayAccess || $data instanceof Traversable)) {
  511. $data = get_object_vars($data);
  512. }
  513. }
  514. if (empty($data)) {
  515. return null;
  516. }
  517. if (is_string($path) && strpos($path, '{') !== false) {
  518. $path = String::tokenize($path, '.', '{', '}');
  519. } elseif (is_string($path)) {
  520. $path = explode('.', $path);
  521. }
  522. $tmp = array();
  523. if (empty($path)) {
  524. return null;
  525. }
  526. foreach ($path as $i => $key) {
  527. if (is_numeric($key) && intval($key) > 0 || $key === '0') {
  528. if (isset($data[intval($key)])) {
  529. $data = $data[intval($key)];
  530. } else {
  531. return null;
  532. }
  533. } elseif ($key === '{n}') {
  534. foreach ($data as $j => $val) {
  535. if (is_int($j)) {
  536. $tmpPath = array_slice($path, $i + 1);
  537. if (empty($tmpPath)) {
  538. $tmp[] = $val;
  539. } else {
  540. $tmp[] = Set::classicExtract($val, $tmpPath);
  541. }
  542. }
  543. }
  544. return $tmp;
  545. } elseif ($key === '{s}') {
  546. foreach ($data as $j => $val) {
  547. if (is_string($j)) {
  548. $tmpPath = array_slice($path, $i + 1);
  549. if (empty($tmpPath)) {
  550. $tmp[] = $val;
  551. } else {
  552. $tmp[] = Set::classicExtract($val, $tmpPath);
  553. }
  554. }
  555. }
  556. return $tmp;
  557. } elseif (false !== strpos($key, '{') && false !== strpos($key, '}')) {
  558. $pattern = substr($key, 1, -1);
  559. foreach ($data as $j => $val) {
  560. if (preg_match('/^' . $pattern . '/s', $j) !== 0) {
  561. $tmpPath = array_slice($path, $i + 1);
  562. if (empty($tmpPath)) {
  563. $tmp[$j] = $val;
  564. } else {
  565. $tmp[$j] = Set::classicExtract($val, $tmpPath);
  566. }
  567. }
  568. }
  569. return $tmp;
  570. } else {
  571. if (isset($data[$key])) {
  572. $data = $data[$key];
  573. } else {
  574. return null;
  575. }
  576. }
  577. }
  578. return $data;
  579. }
  580. /**
  581. * Inserts $data into an array as defined by $path.
  582. *
  583. * @param mixed $list Where to insert into
  584. * @param mixed $path A dot-separated string.
  585. * @param array $data Data to insert
  586. * @return array
  587. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::insert
  588. */
  589. public static function insert($list, $path, $data = null) {
  590. return Hash::insert($list, $path, $data);
  591. }
  592. /**
  593. * Removes an element from a Set or array as defined by $path.
  594. *
  595. * @param mixed $list From where to remove
  596. * @param mixed $path A dot-separated string.
  597. * @return array Array with $path removed from its value
  598. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::remove
  599. */
  600. public static function remove($list, $path = null) {
  601. return Hash::remove($list, $path);
  602. }
  603. /**
  604. * Checks if a particular path is set in an array
  605. *
  606. * @param mixed $data Data to check on
  607. * @param mixed $path A dot-separated string.
  608. * @return boolean true if path is found, false otherwise
  609. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::check
  610. */
  611. public static function check($data, $path = null) {
  612. if (empty($path)) {
  613. return $data;
  614. }
  615. if (!is_array($path)) {
  616. $path = explode('.', $path);
  617. }
  618. foreach ($path as $i => $key) {
  619. if (is_numeric($key) && intval($key) > 0 || $key === '0') {
  620. $key = intval($key);
  621. }
  622. if ($i === count($path) - 1) {
  623. return (is_array($data) && array_key_exists($key, $data));
  624. }
  625. if (!is_array($data) || !array_key_exists($key, $data)) {
  626. return false;
  627. }
  628. $data =& $data[$key];
  629. }
  630. return true;
  631. }
  632. /**
  633. * Computes the difference between a Set and an array, two Sets, or two arrays
  634. *
  635. * @param mixed $val1 First value
  636. * @param mixed $val2 Second value
  637. * @return array Returns the key => value pairs that are not common in $val1 and $val2
  638. * The expression for this function is($val1 - $val2) + ($val2 - ($val1 - $val2))
  639. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::diff
  640. */
  641. public static function diff($val1, $val2 = null) {
  642. if (empty($val1)) {
  643. return (array)$val2;
  644. }
  645. if (empty($val2)) {
  646. return (array)$val1;
  647. }
  648. $intersection = array_intersect_key($val1, $val2);
  649. while (($key = key($intersection)) !== null) {
  650. if ($val1[$key] == $val2[$key]) {
  651. unset($val1[$key]);
  652. unset($val2[$key]);
  653. }
  654. next($intersection);
  655. }
  656. return $val1 + $val2;
  657. }
  658. /**
  659. * Determines if one Set or array contains the exact keys and values of another.
  660. *
  661. * @param array $val1 First value
  662. * @param array $val2 Second value
  663. * @return boolean true if $val1 contains $val2, false otherwise
  664. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::contains
  665. */
  666. public static function contains($val1, $val2 = null) {
  667. if (empty($val1) || empty($val2)) {
  668. return false;
  669. }
  670. foreach ($val2 as $key => $val) {
  671. if (is_numeric($key)) {
  672. Set::contains($val, $val1);
  673. } else {
  674. if (!isset($val1[$key]) || $val1[$key] != $val) {
  675. return false;
  676. }
  677. }
  678. }
  679. return true;
  680. }
  681. /**
  682. * Counts the dimensions of an array. If $all is set to false (which is the default) it will
  683. * only consider the dimension of the first element in the array.
  684. *
  685. * @param array $array Array to count dimensions on
  686. * @param boolean $all Set to true to count the dimension considering all elements in array
  687. * @param integer $count Start the dimension count at this number
  688. * @return integer The number of dimensions in $array
  689. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::countDim
  690. */
  691. public static function countDim($array = null, $all = false, $count = 0) {
  692. if ($all) {
  693. $depth = array($count);
  694. if (is_array($array) && reset($array) !== false) {
  695. foreach ($array as $value) {
  696. $depth[] = Set::countDim($value, true, $count + 1);
  697. }
  698. }
  699. $return = max($depth);
  700. } else {
  701. if (is_array(reset($array))) {
  702. $return = Set::countDim(reset($array)) + 1;
  703. } else {
  704. $return = 1;
  705. }
  706. }
  707. return $return;
  708. }
  709. /**
  710. * Normalizes a string or array list.
  711. *
  712. * @param mixed $list List to normalize
  713. * @param boolean $assoc If true, $list will be converted to an associative array
  714. * @param string $sep If $list is a string, it will be split into an array with $sep
  715. * @param boolean $trim If true, separated strings will be trimmed
  716. * @return array
  717. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::normalize
  718. */
  719. public static function normalize($list, $assoc = true, $sep = ',', $trim = true) {
  720. if (is_string($list)) {
  721. $list = explode($sep, $list);
  722. if ($trim) {
  723. foreach ($list as $key => $value) {
  724. $list[$key] = trim($value);
  725. }
  726. }
  727. if ($assoc) {
  728. return Hash::normalize($list);
  729. }
  730. } elseif (is_array($list)) {
  731. $list = Hash::normalize($list, $assoc);
  732. }
  733. return $list;
  734. }
  735. /**
  736. * Creates an associative array using a $path1 as the path to build its keys, and optionally
  737. * $path2 as path to get the values. If $path2 is not specified, all values will be initialized
  738. * to null (useful for Set::merge). You can optionally group the values by what is obtained when
  739. * following the path specified in $groupPath.
  740. *
  741. * @param mixed $data Array or object from where to extract keys and values
  742. * @param mixed $path1 As an array, or as a dot-separated string.
  743. * @param mixed $path2 As an array, or as a dot-separated string.
  744. * @param string $groupPath As an array, or as a dot-separated string.
  745. * @return array Combined array
  746. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::combine
  747. */
  748. public static function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
  749. if (empty($data)) {
  750. return array();
  751. }
  752. if (is_object($data)) {
  753. if (!($data instanceof ArrayAccess || $data instanceof Traversable)) {
  754. $data = get_object_vars($data);
  755. }
  756. }
  757. if (is_array($path1)) {
  758. $format = array_shift($path1);
  759. $keys = Set::format($data, $format, $path1);
  760. } else {
  761. $keys = Set::extract($data, $path1);
  762. }
  763. if (empty($keys)) {
  764. return array();
  765. }
  766. if (!empty($path2) && is_array($path2)) {
  767. $format = array_shift($path2);
  768. $vals = Set::format($data, $format, $path2);
  769. } elseif (!empty($path2)) {
  770. $vals = Set::extract($data, $path2);
  771. } else {
  772. $count = count($keys);
  773. for ($i = 0; $i < $count; $i++) {
  774. $vals[$i] = null;
  775. }
  776. }
  777. if ($groupPath != null) {
  778. $group = Set::extract($data, $groupPath);
  779. if (!empty($group)) {
  780. $c = count($keys);
  781. for ($i = 0; $i < $c; $i++) {
  782. if (!isset($group[$i])) {
  783. $group[$i] = 0;
  784. }
  785. if (!isset($out[$group[$i]])) {
  786. $out[$group[$i]] = array();
  787. }
  788. $out[$group[$i]][$keys[$i]] = $vals[$i];
  789. }
  790. return $out;
  791. }
  792. }
  793. if (empty($vals)) {
  794. return array();
  795. }
  796. return array_combine($keys, $vals);
  797. }
  798. /**
  799. * Converts an object into an array.
  800. * @param object $object Object to reverse
  801. * @return array Array representation of given object
  802. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::reverse
  803. */
  804. public static function reverse($object) {
  805. $out = array();
  806. if ($object instanceof SimpleXMLElement) {
  807. return Xml::toArray($object);
  808. } elseif (is_object($object)) {
  809. $keys = get_object_vars($object);
  810. if (isset($keys['_name_'])) {
  811. $identity = $keys['_name_'];
  812. unset($keys['_name_']);
  813. }
  814. $new = array();
  815. foreach ($keys as $key => $value) {
  816. if (is_array($value)) {
  817. $new[$key] = (array)Set::reverse($value);
  818. } else {
  819. // @codingStandardsIgnoreStart Legacy junk
  820. if (isset($value->_name_)) {
  821. $new = array_merge($new, Set::reverse($value));
  822. } else {
  823. $new[$key] = Set::reverse($value);
  824. }
  825. // @codingStandardsIgnoreEnd
  826. }
  827. }
  828. if (isset($identity)) {
  829. $out[$identity] = $new;
  830. } else {
  831. $out = $new;
  832. }
  833. } elseif (is_array($object)) {
  834. foreach ($object as $key => $value) {
  835. $out[$key] = Set::reverse($value);
  836. }
  837. } else {
  838. $out = $object;
  839. }
  840. return $out;
  841. }
  842. /**
  843. * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
  844. * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
  845. * array('0.Foo.Bar' => 'Far').
  846. *
  847. * @param array $data Array to flatten
  848. * @param string $separator String used to separate array key elements in a path, defaults to '.'
  849. * @return array
  850. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::flatten
  851. */
  852. public static function flatten($data, $separator = '.') {
  853. return Hash::flatten($data, $separator);
  854. }
  855. /**
  856. * Expand/unflattens an string to an array
  857. *
  858. * For example, unflattens an array that was collapsed with `Set::flatten()`
  859. * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes
  860. * `array(array('Foo' => array('Bar' => 'Far')))`.
  861. *
  862. * @param array $data Flattened array
  863. * @param string $separator The delimiter used
  864. * @return array
  865. */
  866. public static function expand($data, $separator = '.') {
  867. return Hash::expand($data, $separator);
  868. }
  869. /**
  870. * Flattens an array for sorting
  871. *
  872. * @param array $results
  873. * @param string $key
  874. * @return array
  875. */
  876. protected static function _flatten($results, $key = null) {
  877. $stack = array();
  878. foreach ($results as $k => $r) {
  879. $id = $k;
  880. if (!is_null($key)) {
  881. $id = $key;
  882. }
  883. if (is_array($r) && !empty($r)) {
  884. $stack = array_merge($stack, Set::_flatten($r, $id));
  885. } else {
  886. $stack[] = array('id' => $id, 'value' => $r);
  887. }
  888. }
  889. return $stack;
  890. }
  891. /**
  892. * Sorts an array by any value, determined by a Set-compatible path
  893. *
  894. * @param array $data An array of data to sort
  895. * @param string $path A Set-compatible path to the array value
  896. * @param string $dir Direction of sorting - either ascending (ASC), or descending (DESC)
  897. * @return array Sorted array of data
  898. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::sort
  899. */
  900. public static function sort($data, $path, $dir) {
  901. $originalKeys = array_keys($data);
  902. $numeric = false;
  903. if (is_numeric(implode('', $originalKeys))) {
  904. $data = array_values($data);
  905. $numeric = true;
  906. }
  907. $result = Set::_flatten(Set::extract($data, $path));
  908. list($keys, $values) = array(Set::extract($result, '{n}.id'), Set::extract($result, '{n}.value'));
  909. $dir = strtolower($dir);
  910. if ($dir === 'asc') {
  911. $dir = SORT_ASC;
  912. } elseif ($dir === 'desc') {
  913. $dir = SORT_DESC;
  914. }
  915. array_multisort($values, $dir, $keys, $dir);
  916. $sorted = array();
  917. $keys = array_unique($keys);
  918. foreach ($keys as $k) {
  919. if ($numeric) {
  920. $sorted[] = $data[$k];
  921. } else {
  922. if (isset($originalKeys[$k])) {
  923. $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
  924. } else {
  925. $sorted[$k] = $data[$k];
  926. }
  927. }
  928. }
  929. return $sorted;
  930. }
  931. /**
  932. * Allows the application of a callback method to elements of an
  933. * array extracted by a Set::extract() compatible path.
  934. *
  935. * @param mixed $path Set-compatible path to the array value
  936. * @param array $data An array of data to extract from & then process with the $callback.
  937. * @param mixed $callback Callback method to be applied to extracted data.
  938. * See http://ca2.php.net/manual/en/language.pseudo-types.php#language.types.callback for examples
  939. * of callback formats.
  940. * @param array $options Options are:
  941. * - type : can be pass, map, or reduce. Map will handoff the given callback
  942. * to array_map, reduce will handoff to array_reduce, and pass will
  943. * use call_user_func_array().
  944. * @return mixed Result of the callback when applied to extracted data
  945. * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::apply
  946. */
  947. public static function apply($path, $data, $callback, $options = array()) {
  948. $defaults = array('type' => 'pass');
  949. $options = array_merge($defaults, $options);
  950. $extracted = Set::extract($path, $data);
  951. if ($options['type'] === 'map') {
  952. return array_map($callback, $extracted);
  953. } elseif ($options['type'] === 'reduce') {
  954. return array_reduce($extracted, $callback);
  955. } elseif ($options['type'] === 'pass') {
  956. return call_user_func_array($callback, array($extracted));
  957. }
  958. return null;
  959. }
  960. /**
  961. * Takes in a flat array and returns a nested array
  962. *
  963. * @param mixed $data
  964. * @param array $options Options are:
  965. * children - the key name to use in the resultset for children
  966. * idPath - the path to a key that identifies each entry
  967. * parentPath - the path to a key that identifies the parent of each entry
  968. * root - the id of the desired top-most result
  969. * @return array of results, nested
  970. * @link
  971. */
  972. public static function nest($data, $options = array()) {
  973. if (!$data) {
  974. return $data;
  975. }
  976. $alias = key(current($data));
  977. $options += array(
  978. 'idPath' => "/$alias/id",
  979. 'parentPath' => "/$alias/parent_id",
  980. 'children' => 'children',
  981. 'root' => null
  982. );
  983. $return = $idMap = array();
  984. $ids = Set::extract($data, $options['idPath']);
  985. $idKeys = explode('/', trim($options['idPath'], '/'));
  986. $parentKeys = explode('/', trim($options['parentPath'], '/'));
  987. foreach ($data as $result) {
  988. $result[$options['children']] = array();
  989. $id = Set::get($result, $idKeys);
  990. $parentId = Set::get($result, $parentKeys);
  991. if (isset($idMap[$id][$options['children']])) {
  992. $idMap[$id] = array_merge($result, (array)$idMap[$id]);
  993. } else {
  994. $idMap[$id] = array_merge($result, array($options['children'] => array()));
  995. }
  996. if (!$parentId || !in_array($parentId, $ids)) {
  997. $return[] =& $idMap[$id];
  998. } else {
  999. $idMap[$parentId][$options['children']][] =& $idMap[$id];
  1000. }
  1001. }
  1002. if ($options['root']) {
  1003. $root = $options['root'];
  1004. } else {
  1005. $root = Set::get($return[0], $parentKeys);
  1006. }
  1007. foreach ($return as $i => $result) {
  1008. $id = Set::get($result, $idKeys);
  1009. $parentId = Set::get($result, $parentKeys);
  1010. if ($id !== $root && $parentId != $root) {
  1011. unset($return[$i]);
  1012. }
  1013. }
  1014. return array_values($return);
  1015. }
  1016. /**
  1017. * Return the value at the specified position
  1018. *
  1019. * @param mixed $input an array
  1020. * @param mixed $path string or array of array keys
  1021. * @return the value at the specified position or null if it doesn't exist
  1022. */
  1023. public static function get($input, $path = null) {
  1024. if (is_string($path)) {
  1025. if (strpos($path, '/') !== false) {
  1026. $keys = explode('/', trim($path, '/'));
  1027. } else {
  1028. $keys = explode('.', trim($path, '.'));
  1029. }
  1030. } else {
  1031. $keys = $path;
  1032. }
  1033. return Hash::get($input, $keys);
  1034. }
  1035. }