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