1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @package Cake.Utility
13: * @since CakePHP(tm) v 2.2.0
14: * @license https://opensource.org/licenses/mit-license.php MIT License
15: */
16:
17: App::uses('CakeText', 'Utility');
18:
19: /**
20: * Library of array functions for manipulating and extracting data
21: * from arrays or 'sets' of data.
22: *
23: * `Hash` provides an improved interface, more consistent and
24: * predictable set of features over `Set`. While it lacks the spotty
25: * support for pseudo Xpath, its more fully featured dot notation provides
26: * similar features in a more consistent implementation.
27: *
28: * @package Cake.Utility
29: */
30: class Hash {
31:
32: /**
33: * Get a single value specified by $path out of $data.
34: * Does not support the full dot notation feature set,
35: * but is faster for simple read operations.
36: *
37: * @param array $data Array of data to operate on.
38: * @param string|array $path The path being searched for. Either a dot
39: * separated string, or an array of path segments.
40: * @param mixed $default The return value when the path does not exist
41: * @throws InvalidArgumentException
42: * @return mixed The value fetched from the array, or null.
43: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::get
44: */
45: public static function get(array $data, $path, $default = null) {
46: if (empty($data) || $path === null) {
47: return $default;
48: }
49: if (is_string($path) || is_numeric($path)) {
50: $parts = explode('.', $path);
51: } elseif (is_bool($path) || $path === null) {
52: $parts = array($path);
53: } else {
54: if (!is_array($path)) {
55: throw new InvalidArgumentException(__d('cake_dev',
56: 'Invalid path parameter: %s, should be dot separated path or array.',
57: var_export($path, true)
58: ));
59: }
60: $parts = $path;
61: }
62:
63: foreach ($parts as $key) {
64: if (is_array($data) && isset($data[$key])) {
65: $data =& $data[$key];
66: } else {
67: return $default;
68: }
69: }
70:
71: return $data;
72: }
73:
74: /**
75: * Gets the values from an array matching the $path expression.
76: * The path expression is a dot separated expression, that can contain a set
77: * of patterns and expressions:
78: *
79: * - `{n}` Matches any numeric key, or integer.
80: * - `{s}` Matches any string key.
81: * - `{*}` Matches any value.
82: * - `Foo` Matches any key with the exact same value.
83: *
84: * There are a number of attribute operators:
85: *
86: * - `=`, `!=` Equality.
87: * - `>`, `<`, `>=`, `<=` Value comparison.
88: * - `=/.../` Regular expression pattern match.
89: *
90: * Given a set of User array data, from a `$User->find('all')` call:
91: *
92: * - `1.User.name` Get the name of the user at index 1.
93: * - `{n}.User.name` Get the name of every user in the set of users.
94: * - `{n}.User[id].name` Get the name of every user with an id key.
95: * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2.
96: * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`.
97: *
98: * @param array $data The data to extract from.
99: * @param string $path The path to extract.
100: * @return array An array of the extracted values. Returns an empty array
101: * if there are no matches.
102: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::extract
103: */
104: public static function extract(array $data, $path) {
105: if (empty($path)) {
106: return $data;
107: }
108:
109: // Simple paths.
110: if (!preg_match('/[{\[]/', $path)) {
111: return (array)static::get($data, $path);
112: }
113:
114: if (strpos($path, '[') === false) {
115: $tokens = explode('.', $path);
116: } else {
117: $tokens = CakeText::tokenize($path, '.', '[', ']');
118: }
119:
120: $_key = '__set_item__';
121:
122: $context = array($_key => array($data));
123:
124: foreach ($tokens as $token) {
125: $next = array();
126:
127: list($token, $conditions) = static::_splitConditions($token);
128:
129: foreach ($context[$_key] as $item) {
130: foreach ((array)$item as $k => $v) {
131: if (static::_matchToken($k, $token)) {
132: $next[] = $v;
133: }
134: }
135: }
136:
137: // Filter for attributes.
138: if ($conditions) {
139: $filter = array();
140: foreach ($next as $item) {
141: if (is_array($item) && static::_matches($item, $conditions)) {
142: $filter[] = $item;
143: }
144: }
145: $next = $filter;
146: }
147: $context = array($_key => $next);
148:
149: }
150: return $context[$_key];
151: }
152:
153: /**
154: * Split token conditions
155: *
156: * @param string $token the token being splitted.
157: * @return array array(token, conditions) with token splitted
158: */
159: protected static function _splitConditions($token) {
160: $conditions = false;
161: $position = strpos($token, '[');
162: if ($position !== false) {
163: $conditions = substr($token, $position);
164: $token = substr($token, 0, $position);
165: }
166:
167: return array($token, $conditions);
168: }
169:
170: /**
171: * Check a key against a token.
172: *
173: * @param string $key The key in the array being searched.
174: * @param string $token The token being matched.
175: * @return bool
176: */
177: protected static function _matchToken($key, $token) {
178: switch ($token) {
179: case '{n}':
180: return is_numeric($key);
181: case '{s}':
182: return is_string($key);
183: case '{*}':
184: return true;
185: default:
186: return is_numeric($token) ? ($key == $token) : $key === $token;
187: }
188: }
189:
190: /**
191: * Checks whether or not $data matches the attribute patterns
192: *
193: * @param array $data Array of data to match.
194: * @param string $selector The patterns to match.
195: * @return bool Fitness of expression.
196: */
197: protected static function _matches(array $data, $selector) {
198: preg_match_all(
199: '/(\[ (?P<attr>[^=><!]+?) (\s* (?P<op>[><!]?[=]|[><]) \s* (?P<val>(?:\/.*?\/ | [^\]]+)) )? \])/x',
200: $selector,
201: $conditions,
202: PREG_SET_ORDER
203: );
204:
205: foreach ($conditions as $cond) {
206: $attr = $cond['attr'];
207: $op = isset($cond['op']) ? $cond['op'] : null;
208: $val = isset($cond['val']) ? $cond['val'] : null;
209:
210: // Presence test.
211: if (empty($op) && empty($val) && !isset($data[$attr])) {
212: return false;
213: }
214:
215: // Empty attribute = fail.
216: if (!(isset($data[$attr]) || array_key_exists($attr, $data))) {
217: return false;
218: }
219:
220: $prop = null;
221: if (isset($data[$attr])) {
222: $prop = $data[$attr];
223: }
224: $isBool = is_bool($prop);
225: if ($isBool && is_numeric($val)) {
226: $prop = $prop ? '1' : '0';
227: } elseif ($isBool) {
228: $prop = $prop ? 'true' : 'false';
229: }
230:
231: // Pattern matches and other operators.
232: if ($op === '=' && $val && $val[0] === '/') {
233: if (!preg_match($val, $prop)) {
234: return false;
235: }
236: } elseif (($op === '=' && $prop != $val) ||
237: ($op === '!=' && $prop == $val) ||
238: ($op === '>' && $prop <= $val) ||
239: ($op === '<' && $prop >= $val) ||
240: ($op === '>=' && $prop < $val) ||
241: ($op === '<=' && $prop > $val)
242: ) {
243: return false;
244: }
245:
246: }
247: return true;
248: }
249:
250: /**
251: * Insert $values into an array with the given $path. You can use
252: * `{n}` and `{s}` elements to insert $data multiple times.
253: *
254: * @param array $data The data to insert into.
255: * @param string $path The path to insert at.
256: * @param mixed $values The values to insert.
257: * @return array The data with $values inserted.
258: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::insert
259: */
260: public static function insert(array $data, $path, $values = null) {
261: if (strpos($path, '[') === false) {
262: $tokens = explode('.', $path);
263: } else {
264: $tokens = CakeText::tokenize($path, '.', '[', ']');
265: }
266:
267: if (strpos($path, '{') === false && strpos($path, '[') === false) {
268: return static::_simpleOp('insert', $data, $tokens, $values);
269: }
270:
271: $token = array_shift($tokens);
272: $nextPath = implode('.', $tokens);
273:
274: list($token, $conditions) = static::_splitConditions($token);
275:
276: foreach ($data as $k => $v) {
277: if (static::_matchToken($k, $token)) {
278: if (!$conditions || static::_matches($v, $conditions)) {
279: $data[$k] = $nextPath
280: ? static::insert($v, $nextPath, $values)
281: : array_merge($v, (array)$values);
282: }
283: }
284: }
285: return $data;
286: }
287:
288: /**
289: * Perform a simple insert/remove operation.
290: *
291: * @param string $op The operation to do.
292: * @param array $data The data to operate on.
293: * @param array $path The path to work on.
294: * @param mixed $values The values to insert when doing inserts.
295: * @return array data.
296: */
297: protected static function _simpleOp($op, $data, $path, $values = null) {
298: $_list =& $data;
299:
300: $count = count($path);
301: $last = $count - 1;
302: foreach ($path as $i => $key) {
303: if ($op === 'insert') {
304: if ($i === $last) {
305: $_list[$key] = $values;
306: return $data;
307: }
308: if (!isset($_list[$key])) {
309: $_list[$key] = array();
310: }
311: $_list =& $_list[$key];
312: if (!is_array($_list)) {
313: $_list = array();
314: }
315: } elseif ($op === 'remove') {
316: if ($i === $last) {
317: if (is_array($_list)) {
318: unset($_list[$key]);
319: }
320: return $data;
321: }
322: if (!isset($_list[$key])) {
323: return $data;
324: }
325: $_list =& $_list[$key];
326: }
327: }
328: }
329:
330: /**
331: * Remove data matching $path from the $data array.
332: * You can use `{n}` and `{s}` to remove multiple elements
333: * from $data.
334: *
335: * @param array $data The data to operate on
336: * @param string $path A path expression to use to remove.
337: * @return array The modified array.
338: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::remove
339: */
340: public static function remove(array $data, $path) {
341: if (strpos($path, '[') === false) {
342: $tokens = explode('.', $path);
343: } else {
344: $tokens = CakeText::tokenize($path, '.', '[', ']');
345: }
346:
347: if (strpos($path, '{') === false && strpos($path, '[') === false) {
348: return static::_simpleOp('remove', $data, $tokens);
349: }
350:
351: $token = array_shift($tokens);
352: $nextPath = implode('.', $tokens);
353:
354: list($token, $conditions) = static::_splitConditions($token);
355:
356: foreach ($data as $k => $v) {
357: $match = static::_matchToken($k, $token);
358: if ($match && is_array($v)) {
359: if ($conditions) {
360: if (static::_matches($v, $conditions)) {
361: if ($nextPath !== '') {
362: $data[$k] = static::remove($v, $nextPath);
363: } else {
364: unset($data[$k]);
365: }
366: }
367: } else {
368: $data[$k] = static::remove($v, $nextPath);
369: }
370: if (empty($data[$k])) {
371: unset($data[$k]);
372: }
373: } elseif ($match && $nextPath === '') {
374: unset($data[$k]);
375: }
376: }
377: return $data;
378: }
379:
380: /**
381: * Creates an associative array using `$keyPath` as the path to build its keys, and optionally
382: * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized
383: * to null (useful for Hash::merge). You can optionally group the values by what is obtained when
384: * following the path specified in `$groupPath`.
385: *
386: * @param array $data Array from where to extract keys and values
387: * @param array|string $keyPath A dot-separated string or array for formatting rules.
388: * @param array|string $valuePath A dot-separated string or array for formatting rules.
389: * @param string $groupPath A dot-separated string.
390: * @return array Combined array
391: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::combine
392: * @throws CakeException CakeException When keys and values count is unequal.
393: */
394: public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) {
395: if (empty($data)) {
396: return array();
397: }
398:
399: if (is_array($keyPath)) {
400: $format = array_shift($keyPath);
401: $keys = static::format($data, $keyPath, $format);
402: } else {
403: $keys = static::extract($data, $keyPath);
404: }
405: if (empty($keys)) {
406: return array();
407: }
408:
409: if (!empty($valuePath) && is_array($valuePath)) {
410: $format = array_shift($valuePath);
411: $vals = static::format($data, $valuePath, $format);
412: } elseif (!empty($valuePath)) {
413: $vals = static::extract($data, $valuePath);
414: }
415: if (empty($vals)) {
416: $vals = array_fill(0, count($keys), null);
417: }
418:
419: if (count($keys) !== count($vals)) {
420: throw new CakeException(__d(
421: 'cake_dev',
422: 'Hash::combine() needs an equal number of keys + values.'
423: ));
424: }
425:
426: if ($groupPath !== null) {
427: $group = static::extract($data, $groupPath);
428: if (!empty($group)) {
429: $c = count($keys);
430: for ($i = 0; $i < $c; $i++) {
431: if (!isset($group[$i])) {
432: $group[$i] = 0;
433: }
434: if (!isset($out[$group[$i]])) {
435: $out[$group[$i]] = array();
436: }
437: $out[$group[$i]][$keys[$i]] = $vals[$i];
438: }
439: return $out;
440: }
441: }
442: if (empty($vals)) {
443: return array();
444: }
445: return array_combine($keys, $vals);
446: }
447:
448: /**
449: * Returns a formatted series of values extracted from `$data`, using
450: * `$format` as the format and `$paths` as the values to extract.
451: *
452: * Usage:
453: *
454: * ```
455: * $result = Hash::format($users, array('{n}.User.id', '{n}.User.name'), '%s : %s');
456: * ```
457: *
458: * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do.
459: *
460: * @param array $data Source array from which to extract the data
461: * @param array $paths An array containing one or more Hash::extract()-style key paths
462: * @param string $format Format string into which values will be inserted, see sprintf()
463: * @return array An array of strings extracted from `$path` and formatted with `$format`
464: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format
465: * @see sprintf()
466: * @see Hash::extract()
467: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format
468: */
469: public static function format(array $data, array $paths, $format) {
470: $extracted = array();
471: $count = count($paths);
472:
473: if (!$count) {
474: return null;
475: }
476:
477: for ($i = 0; $i < $count; $i++) {
478: $extracted[] = static::extract($data, $paths[$i]);
479: }
480: $out = array();
481: $data = $extracted;
482: $count = count($data[0]);
483:
484: $countTwo = count($data);
485: for ($j = 0; $j < $count; $j++) {
486: $args = array();
487: for ($i = 0; $i < $countTwo; $i++) {
488: if (array_key_exists($j, $data[$i])) {
489: $args[] = $data[$i][$j];
490: }
491: }
492: $out[] = vsprintf($format, $args);
493: }
494: return $out;
495: }
496:
497: /**
498: * Determines if one array contains the exact keys and values of another.
499: *
500: * @param array $data The data to search through.
501: * @param array $needle The values to file in $data
502: * @return bool true if $data contains $needle, false otherwise
503: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::contains
504: */
505: public static function contains(array $data, array $needle) {
506: if (empty($data) || empty($needle)) {
507: return false;
508: }
509: $stack = array();
510:
511: while (!empty($needle)) {
512: $key = key($needle);
513: $val = $needle[$key];
514: unset($needle[$key]);
515:
516: if (array_key_exists($key, $data) && is_array($val)) {
517: $next = $data[$key];
518: unset($data[$key]);
519:
520: if (!empty($val)) {
521: $stack[] = array($val, $next);
522: }
523: } elseif (!array_key_exists($key, $data) || $data[$key] != $val) {
524: return false;
525: }
526:
527: if (empty($needle) && !empty($stack)) {
528: list($needle, $data) = array_pop($stack);
529: }
530: }
531: return true;
532: }
533:
534: /**
535: * Test whether or not a given path exists in $data.
536: * This method uses the same path syntax as Hash::extract()
537: *
538: * Checking for paths that could target more than one element will
539: * make sure that at least one matching element exists.
540: *
541: * @param array $data The data to check.
542: * @param string $path The path to check for.
543: * @return bool Existence of path.
544: * @see Hash::extract()
545: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::check
546: */
547: public static function check(array $data, $path) {
548: $results = static::extract($data, $path);
549: if (!is_array($results)) {
550: return false;
551: }
552: return count($results) > 0;
553: }
554:
555: /**
556: * Recursively filters a data set.
557: *
558: * @param array $data Either an array to filter, or value when in callback
559: * @param callable $callback A function to filter the data with. Defaults to
560: * `static::_filter()` Which strips out all non-zero empty values.
561: * @return array Filtered array
562: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::filter
563: */
564: public static function filter(array $data, $callback = array('self', '_filter')) {
565: foreach ($data as $k => $v) {
566: if (is_array($v)) {
567: $data[$k] = static::filter($v, $callback);
568: }
569: }
570: return array_filter($data, $callback);
571: }
572:
573: /**
574: * Callback function for filtering.
575: *
576: * @param array $var Array to filter.
577: * @return bool
578: */
579: protected static function _filter($var) {
580: if ($var === 0 || $var === 0.0 || $var === '0' || !empty($var)) {
581: return true;
582: }
583: return false;
584: }
585:
586: /**
587: * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
588: * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
589: * array('0.Foo.Bar' => 'Far').)
590: *
591: * @param array $data Array to flatten
592: * @param string $separator String used to separate array key elements in a path, defaults to '.'
593: * @return array
594: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::flatten
595: */
596: public static function flatten(array $data, $separator = '.') {
597: $result = array();
598: $stack = array();
599: $path = null;
600:
601: reset($data);
602: while (!empty($data)) {
603: $key = key($data);
604: $element = $data[$key];
605: unset($data[$key]);
606:
607: if (is_array($element) && !empty($element)) {
608: if (!empty($data)) {
609: $stack[] = array($data, $path);
610: }
611: $data = $element;
612: reset($data);
613: $path .= $key . $separator;
614: } else {
615: $result[$path . $key] = $element;
616: }
617:
618: if (empty($data) && !empty($stack)) {
619: list($data, $path) = array_pop($stack);
620: reset($data);
621: }
622: }
623: return $result;
624: }
625:
626: /**
627: * Expands a flat array to a nested array.
628: *
629: * For example, unflattens an array that was collapsed with `Hash::flatten()`
630: * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes
631: * `array(array('Foo' => array('Bar' => 'Far')))`.
632: *
633: * @param array $data Flattened array
634: * @param string $separator The delimiter used
635: * @return array
636: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::expand
637: */
638: public static function expand($data, $separator = '.') {
639: $result = array();
640:
641: $stack = array();
642:
643: foreach ($data as $flat => $value) {
644: $keys = explode($separator, $flat);
645: $keys = array_reverse($keys);
646: $child = array(
647: $keys[0] => $value
648: );
649: array_shift($keys);
650: foreach ($keys as $k) {
651: $child = array(
652: $k => $child
653: );
654: }
655:
656: $stack[] = array($child, &$result);
657:
658: while (!empty($stack)) {
659: foreach ($stack as $curKey => &$curMerge) {
660: foreach ($curMerge[0] as $key => &$val) {
661: if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) {
662: $stack[] = array(&$val, &$curMerge[1][$key]);
663: } elseif ((int)$key === $key && isset($curMerge[1][$key])) {
664: $curMerge[1][] = $val;
665: } else {
666: $curMerge[1][$key] = $val;
667: }
668: }
669: unset($stack[$curKey]);
670: }
671: unset($curMerge);
672: }
673: }
674: return $result;
675: }
676:
677: /**
678: * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`.
679: *
680: * The difference between this method and the built-in ones, is that if an array key contains another array, then
681: * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for
682: * keys that contain scalar values (unlike `array_merge_recursive`).
683: *
684: * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays.
685: *
686: * @param array $data Array to be merged
687: * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged
688: * @return array Merged array
689: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge
690: */
691: public static function merge(array $data, $merge) {
692: $args = array_slice(func_get_args(), 1);
693: $return = $data;
694:
695: foreach ($args as &$curArg) {
696: $stack[] = array((array)$curArg, &$return);
697: }
698: unset($curArg);
699:
700: while (!empty($stack)) {
701: foreach ($stack as $curKey => &$curMerge) {
702: foreach ($curMerge[0] as $key => &$val) {
703: if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) {
704: $stack[] = array(&$val, &$curMerge[1][$key]);
705: } elseif ((int)$key === $key && isset($curMerge[1][$key])) {
706: $curMerge[1][] = $val;
707: } else {
708: $curMerge[1][$key] = $val;
709: }
710: }
711: unset($stack[$curKey]);
712: }
713: unset($curMerge);
714: }
715: return $return;
716: }
717:
718: /**
719: * Checks to see if all the values in the array are numeric
720: *
721: * @param array $data The array to check.
722: * @return bool true if values are numeric, false otherwise
723: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::numeric
724: */
725: public static function numeric(array $data) {
726: if (empty($data)) {
727: return false;
728: }
729: return $data === array_filter($data, 'is_numeric');
730: }
731:
732: /**
733: * Counts the dimensions of an array.
734: * Only considers the dimension of the first element in the array.
735: *
736: * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions()
737: * to get the dimensions of the array.
738: *
739: * @param array $data Array to count dimensions on
740: * @return int The number of dimensions in $data
741: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::dimensions
742: */
743: public static function dimensions(array $data) {
744: if (empty($data)) {
745: return 0;
746: }
747: reset($data);
748: $depth = 1;
749: while ($elem = array_shift($data)) {
750: if (is_array($elem)) {
751: $depth += 1;
752: $data =& $elem;
753: } else {
754: break;
755: }
756: }
757: return $depth;
758: }
759:
760: /**
761: * Counts the dimensions of *all* array elements. Useful for finding the maximum
762: * number of dimensions in a mixed array.
763: *
764: * @param array $data Array to count dimensions on
765: * @return int The maximum number of dimensions in $data
766: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::maxDimensions
767: */
768: public static function maxDimensions($data) {
769: $depth = array();
770: if (is_array($data) && reset($data) !== false) {
771: foreach ($data as $value) {
772: $depth[] = static::maxDimensions($value) + 1;
773: }
774: }
775: return empty($depth) ? 0 : max($depth);
776: }
777:
778: /**
779: * Map a callback across all elements in a set.
780: * Can be provided a path to only modify slices of the set.
781: *
782: * @param array $data The data to map over, and extract data out of.
783: * @param string $path The path to extract for mapping over.
784: * @param callable $function The function to call on each extracted value.
785: * @return array An array of the modified values.
786: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::map
787: */
788: public static function map(array $data, $path, $function) {
789: $values = (array)static::extract($data, $path);
790: return array_map($function, $values);
791: }
792:
793: /**
794: * Reduce a set of extracted values using `$function`.
795: *
796: * @param array $data The data to reduce.
797: * @param string $path The path to extract from $data.
798: * @param callable $function The function to call on each extracted value.
799: * @return mixed The reduced value.
800: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::reduce
801: */
802: public static function reduce(array $data, $path, $function) {
803: $values = (array)static::extract($data, $path);
804: return array_reduce($values, $function);
805: }
806:
807: /**
808: * Apply a callback to a set of extracted values using `$function`.
809: * The function will get the extracted values as the first argument.
810: *
811: * ### Example
812: *
813: * You can easily count the results of an extract using apply().
814: * For example to count the comments on an Article:
815: *
816: * ```
817: * $count = Hash::apply($data, 'Article.Comment.{n}', 'count');
818: * ```
819: *
820: * You could also use a function like `array_sum` to sum the results.
821: *
822: * ```
823: * $total = Hash::apply($data, '{n}.Item.price', 'array_sum');
824: * ```
825: *
826: * @param array $data The data to reduce.
827: * @param string $path The path to extract from $data.
828: * @param callable $function The function to call on each extracted value.
829: * @return mixed The results of the applied method.
830: */
831: public static function apply(array $data, $path, $function) {
832: $values = (array)static::extract($data, $path);
833: return call_user_func($function, $values);
834: }
835:
836: /**
837: * Sorts an array by any value, determined by a Hash-compatible path
838: *
839: * ### Sort directions
840: *
841: * - `asc` Sort ascending.
842: * - `desc` Sort descending.
843: *
844: * ### Sort types
845: *
846: * - `regular` For regular sorting (don't change types)
847: * - `numeric` Compare values numerically
848: * - `string` Compare values as strings
849: * - `locale` Compare items as strings, based on the current locale
850: * - `natural` Compare items as strings using "natural ordering" in a human friendly way.
851: * Will sort foo10 below foo2 as an example. Requires PHP 5.4 or greater or it will fallback to 'regular'
852: *
853: * To do case insensitive sorting, pass the type as an array as follows:
854: *
855: * ```
856: * array('type' => 'regular', 'ignoreCase' => true)
857: * ```
858: *
859: * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option
860: * defaults to `false`.
861: *
862: * @param array $data An array of data to sort
863: * @param string $path A Hash-compatible path to the array value
864: * @param string $dir See directions above. Defaults to 'asc'.
865: * @param array|string $type See direction types above. Defaults to 'regular'.
866: * @return array Sorted array of data
867: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort
868: */
869: public static function sort(array $data, $path, $dir = 'asc', $type = 'regular') {
870: if (empty($data)) {
871: return array();
872: }
873: $originalKeys = array_keys($data);
874: $numeric = is_numeric(implode('', $originalKeys));
875: if ($numeric) {
876: $data = array_values($data);
877: }
878: $sortValues = static::extract($data, $path);
879: $dataCount = count($data);
880:
881: // Make sortValues match the data length, as some keys could be missing
882: // the sorted value path.
883: $missingData = count($sortValues) < $dataCount;
884: if ($missingData && $numeric) {
885: // Get the path without the leading '{n}.'
886: $itemPath = substr($path, 4);
887: foreach ($data as $key => $value) {
888: $sortValues[$key] = static::get($value, $itemPath);
889: }
890: } elseif ($missingData) {
891: $sortValues = array_pad($sortValues, $dataCount, null);
892: }
893: $result = static::_squash($sortValues);
894: $keys = static::extract($result, '{n}.id');
895: $values = static::extract($result, '{n}.value');
896:
897: $dir = strtolower($dir);
898: $ignoreCase = false;
899:
900: // $type can be overloaded for case insensitive sort
901: if (is_array($type)) {
902: $type += array('ignoreCase' => false, 'type' => 'regular');
903: $ignoreCase = $type['ignoreCase'];
904: $type = $type['type'];
905: }
906: $type = strtolower($type);
907:
908: if ($type === 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) {
909: $type = 'regular';
910: }
911:
912: if ($dir === 'asc') {
913: $dir = SORT_ASC;
914: } else {
915: $dir = SORT_DESC;
916: }
917: if ($type === 'numeric') {
918: $type = SORT_NUMERIC;
919: } elseif ($type === 'string') {
920: $type = SORT_STRING;
921: } elseif ($type === 'natural') {
922: $type = SORT_NATURAL;
923: } elseif ($type === 'locale') {
924: $type = SORT_LOCALE_STRING;
925: } else {
926: $type = SORT_REGULAR;
927: }
928:
929: if ($ignoreCase) {
930: $values = array_map('mb_strtolower', $values);
931: }
932: array_multisort($values, $dir, $type, $keys, $dir);
933:
934: $sorted = array();
935: $keys = array_unique($keys);
936:
937: foreach ($keys as $k) {
938: if ($numeric) {
939: $sorted[] = $data[$k];
940: continue;
941: }
942: if (isset($originalKeys[$k])) {
943: $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
944: } else {
945: $sorted[$k] = $data[$k];
946: }
947: }
948: return $sorted;
949: }
950:
951: /**
952: * Helper method for sort()
953: * Squashes an array to a single hash so it can be sorted.
954: *
955: * @param array $data The data to squash.
956: * @param string $key The key for the data.
957: * @return array
958: */
959: protected static function _squash($data, $key = null) {
960: $stack = array();
961: foreach ($data as $k => $r) {
962: $id = $k;
963: if ($key !== null) {
964: $id = $key;
965: }
966: if (is_array($r) && !empty($r)) {
967: $stack = array_merge($stack, static::_squash($r, $id));
968: } else {
969: $stack[] = array('id' => $id, 'value' => $r);
970: }
971: }
972: return $stack;
973: }
974:
975: /**
976: * Computes the difference between two complex arrays.
977: * This method differs from the built-in array_diff() in that it will preserve keys
978: * and work on multi-dimensional arrays.
979: *
980: * @param array $data First value
981: * @param array $compare Second value
982: * @return array Returns the key => value pairs that are not common in $data and $compare
983: * The expression for this function is ($data - $compare) + ($compare - ($data - $compare))
984: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff
985: */
986: public static function diff(array $data, $compare) {
987: if (empty($data)) {
988: return (array)$compare;
989: }
990: if (empty($compare)) {
991: return (array)$data;
992: }
993: $intersection = array_intersect_key($data, $compare);
994: while (($key = key($intersection)) !== null) {
995: if ($data[$key] == $compare[$key]) {
996: unset($data[$key]);
997: unset($compare[$key]);
998: }
999: next($intersection);
1000: }
1001: return $data + $compare;
1002: }
1003:
1004: /**
1005: * Merges the difference between $data and $compare onto $data.
1006: *
1007: * @param array $data The data to append onto.
1008: * @param array $compare The data to compare and append onto.
1009: * @return array The merged array.
1010: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::mergeDiff
1011: */
1012: public static function mergeDiff(array $data, $compare) {
1013: if (empty($data) && !empty($compare)) {
1014: return $compare;
1015: }
1016: if (empty($compare)) {
1017: return $data;
1018: }
1019: foreach ($compare as $key => $value) {
1020: if (!array_key_exists($key, $data)) {
1021: $data[$key] = $value;
1022: } elseif (is_array($value)) {
1023: $data[$key] = static::mergeDiff($data[$key], $compare[$key]);
1024: }
1025: }
1026: return $data;
1027: }
1028:
1029: /**
1030: * Normalizes an array, and converts it to a standard format.
1031: *
1032: * @param array $data List to normalize
1033: * @param bool $assoc If true, $data will be converted to an associative array.
1034: * @return array
1035: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize
1036: */
1037: public static function normalize(array $data, $assoc = true) {
1038: $keys = array_keys($data);
1039: $count = count($keys);
1040: $numeric = true;
1041:
1042: if (!$assoc) {
1043: for ($i = 0; $i < $count; $i++) {
1044: if (!is_int($keys[$i])) {
1045: $numeric = false;
1046: break;
1047: }
1048: }
1049: }
1050: if (!$numeric || $assoc) {
1051: $newList = array();
1052: for ($i = 0; $i < $count; $i++) {
1053: if (is_int($keys[$i])) {
1054: $newList[$data[$keys[$i]]] = null;
1055: } else {
1056: $newList[$keys[$i]] = $data[$keys[$i]];
1057: }
1058: }
1059: $data = $newList;
1060: }
1061: return $data;
1062: }
1063:
1064: /**
1065: * Takes in a flat array and returns a nested array
1066: *
1067: * ### Options:
1068: *
1069: * - `children` The key name to use in the resultset for children.
1070: * - `idPath` The path to a key that identifies each entry. Should be
1071: * compatible with Hash::extract(). Defaults to `{n}.$alias.id`
1072: * - `parentPath` The path to a key that identifies the parent of each entry.
1073: * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id`
1074: * - `root` The id of the desired top-most result.
1075: *
1076: * @param array $data The data to nest.
1077: * @param array $options Options are:
1078: * @return array of results, nested
1079: * @see Hash::extract()
1080: * @throws InvalidArgumentException When providing invalid data.
1081: * @link https://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::nest
1082: */
1083: public static function nest(array $data, $options = array()) {
1084: if (!$data) {
1085: return $data;
1086: }
1087:
1088: $alias = key(current($data));
1089: $options += array(
1090: 'idPath' => "{n}.$alias.id",
1091: 'parentPath' => "{n}.$alias.parent_id",
1092: 'children' => 'children',
1093: 'root' => null
1094: );
1095:
1096: $return = $idMap = array();
1097: $ids = static::extract($data, $options['idPath']);
1098:
1099: $idKeys = explode('.', $options['idPath']);
1100: array_shift($idKeys);
1101:
1102: $parentKeys = explode('.', $options['parentPath']);
1103: array_shift($parentKeys);
1104:
1105: foreach ($data as $result) {
1106: $result[$options['children']] = array();
1107:
1108: $id = static::get($result, $idKeys);
1109: $parentId = static::get($result, $parentKeys);
1110:
1111: if (isset($idMap[$id][$options['children']])) {
1112: $idMap[$id] = array_merge($result, (array)$idMap[$id]);
1113: } else {
1114: $idMap[$id] = array_merge($result, array($options['children'] => array()));
1115: }
1116: if (!$parentId || !in_array($parentId, $ids)) {
1117: $return[] =& $idMap[$id];
1118: } else {
1119: $idMap[$parentId][$options['children']][] =& $idMap[$id];
1120: }
1121: }
1122:
1123: if (!$return) {
1124: throw new InvalidArgumentException(__d('cake_dev',
1125: 'Invalid data array to nest.'
1126: ));
1127: }
1128:
1129: if ($options['root']) {
1130: $root = $options['root'];
1131: } else {
1132: $root = static::get($return[0], $parentKeys);
1133: }
1134:
1135: foreach ($return as $i => $result) {
1136: $id = static::get($result, $idKeys);
1137: $parentId = static::get($result, $parentKeys);
1138: if ($id !== $root && $parentId != $root) {
1139: unset($return[$i]);
1140: }
1141: }
1142: return array_values($return);
1143: }
1144:
1145: }
1146: