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