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