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