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