1: <?php
2: /**
3: * Library of array functions for Cake.
4: *
5: * PHP 5
6: *
7: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8: * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
9: *
10: * Licensed under The MIT License
11: * Redistributions of files must retain the above copyright notice.
12: *
13: * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
14: * @link http://cakephp.org CakePHP(tm) Project
15: * @package Cake.Utility
16: * @since CakePHP(tm) v 1.2.0
17: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
18: */
19:
20: App::uses('String', 'Utility');
21:
22: /**
23: * Class used for manipulation of arrays.
24: *
25: * @package Cake.Utility
26: */
27: class Set {
28:
29: /**
30: * This function can be thought of as a hybrid between PHP's array_merge and array_merge_recursive. The difference
31: * to the two is that if an array key contains another array then the function behaves recursive (unlike array_merge)
32: * but does not do if for keys containing strings (unlike array_merge_recursive).
33: *
34: * Since this method emulates `array_merge`, it will re-order numeric keys. When combined with out of
35: * order numeric keys containing arrays, results can be lossy.
36: *
37: * Note: This function will work with an unlimited amount of arguments and typecasts non-array
38: * parameters into arrays.
39: *
40: * @param array $arr1 Array to be merged
41: * @param array $arr2 Array to merge with
42: * @return array Merged array
43: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::merge
44: */
45: public static function merge($arr1, $arr2 = null) {
46: $args = func_get_args();
47:
48: $r = (array)current($args);
49: while (($arg = next($args)) !== false) {
50: foreach ((array)$arg as $key => $val) {
51: if (!empty($r[$key]) && is_array($r[$key]) && is_array($val)) {
52: $r[$key] = Set::merge($r[$key], $val);
53: } elseif (is_int($key)) {
54: $r[] = $val;
55: } else {
56: $r[$key] = $val;
57: }
58: }
59: }
60: return $r;
61: }
62:
63: /**
64: * Filters empty elements out of a route array, excluding '0'.
65: *
66: * @param array $var Either an array to filter, or value when in callback
67: * @return mixed Either filtered array, or true/false when in callback
68: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::filter
69: */
70: public static function filter(array $var) {
71: foreach ($var as $k => $v) {
72: if (is_array($v)) {
73: $var[$k] = Set::filter($v);
74: }
75: }
76: return array_filter($var, array('Set', '_filter'));
77: }
78:
79: /**
80: * Set::filter callback function
81: *
82: * @param array $var Array to filter.
83: * @return boolean
84: */
85: protected static function _filter($var) {
86: if ($var === 0 || $var === '0' || !empty($var)) {
87: return true;
88: }
89: return false;
90: }
91:
92: /**
93: * Pushes the differences in $array2 onto the end of $array
94: *
95: * @param mixed $array Original array
96: * @param mixed $array2 Differences to push
97: * @return array Combined array
98: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::pushDiff
99: */
100: public static function pushDiff($array, $array2) {
101: if (empty($array) && !empty($array2)) {
102: return $array2;
103: }
104: if (!empty($array) && !empty($array2)) {
105: foreach ($array2 as $key => $value) {
106: if (!array_key_exists($key, $array)) {
107: $array[$key] = $value;
108: } else {
109: if (is_array($value)) {
110: $array[$key] = Set::pushDiff($array[$key], $array2[$key]);
111: }
112: }
113: }
114: }
115: return $array;
116: }
117:
118: /**
119: * Maps the contents of the Set object to an object hierarchy.
120: * Maintains numeric keys as arrays of objects
121: *
122: * @param string $class A class name of the type of object to map to
123: * @param string $tmp A temporary class name used as $class if $class is an array
124: * @return object Hierarchical object
125: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::map
126: */
127: public static function map($class = 'stdClass', $tmp = 'stdClass') {
128: if (is_array($class)) {
129: $val = $class;
130: $class = $tmp;
131: }
132:
133: if (empty($val)) {
134: return null;
135: }
136: return Set::_map($val, $class);
137: }
138:
139: /**
140: * Maps the given value as an object. If $value is an object,
141: * it returns $value. Otherwise it maps $value as an object of
142: * type $class, and if primary assign _name_ $key on first array.
143: * If $value is not empty, it will be used to set properties of
144: * returned object (recursively). If $key is numeric will maintain array
145: * structure
146: *
147: * @param array $array Array to map
148: * @param string $class Class name
149: * @param boolean $primary whether to assign first array key as the _name_
150: * @return mixed Mapped object
151: */
152: protected static function _map(&$array, $class, $primary = false) {
153: if ($class === true) {
154: $out = new stdClass;
155: } else {
156: $out = new $class;
157: }
158: if (is_array($array)) {
159: $keys = array_keys($array);
160: foreach ($array as $key => $value) {
161: if ($keys[0] === $key && $class !== true) {
162: $primary = true;
163: }
164: if (is_numeric($key)) {
165: if (is_object($out)) {
166: $out = get_object_vars($out);
167: }
168: $out[$key] = Set::_map($value, $class);
169: if (is_object($out[$key])) {
170: if ($primary !== true && is_array($value) && Set::countDim($value, true) === 2) {
171: if (!isset($out[$key]->_name_)) {
172: $out[$key]->_name_ = $primary;
173: }
174: }
175: }
176: } elseif (is_array($value)) {
177: if ($primary === true) {
178: // @codingStandardsIgnoreStart Legacy junk
179: if (!isset($out->_name_)) {
180: $out->_name_ = $key;
181: }
182: // @codingStandardsIgnoreEnd
183: $primary = false;
184: foreach ($value as $key2 => $value2) {
185: $out->{$key2} = Set::_map($value2, true);
186: }
187: } else {
188: if (!is_numeric($key)) {
189: $out->{$key} = Set::_map($value, true, $key);
190: if (is_object($out->{$key}) && !is_numeric($key)) {
191: if (!isset($out->{$key}->_name_)) {
192: $out->{$key}->_name_ = $key;
193: }
194: }
195: } else {
196: $out->{$key} = Set::_map($value, true);
197: }
198: }
199: } else {
200: $out->{$key} = $value;
201: }
202: }
203: } else {
204: $out = $array;
205: }
206: return $out;
207: }
208:
209: /**
210: * Checks to see if all the values in the array are numeric
211: *
212: * @param array $array The array to check. If null, the value of the current Set object
213: * @return boolean true if values are numeric, false otherwise
214: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::numeric
215: */
216: public static function numeric($array = null) {
217: if (empty($array)) {
218: return null;
219: }
220:
221: if ($array === range(0, count($array) - 1)) {
222: return true;
223: }
224:
225: $numeric = true;
226: $keys = array_keys($array);
227: $count = count($keys);
228:
229: for ($i = 0; $i < $count; $i++) {
230: if (!is_numeric($array[$keys[$i]])) {
231: $numeric = false;
232: break;
233: }
234: }
235: return $numeric;
236: }
237:
238: /**
239: * Return a value from an array list if the key exists.
240: *
241: * If a comma separated $list is passed arrays are numeric with the key of the first being 0
242: * $list = 'no, yes' would translate to $list = array(0 => 'no', 1 => 'yes');
243: *
244: * If an array is used, keys can be strings example: array('no' => 0, 'yes' => 1);
245: *
246: * $list defaults to 0 = no 1 = yes if param is not passed
247: *
248: * @param mixed $select Key in $list to return
249: * @param mixed $list can be an array or a comma-separated list.
250: * @return string the value of the array key or null if no match
251: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::enum
252: */
253: public static function enum($select, $list = null) {
254: if (empty($list)) {
255: $list = array('no', 'yes');
256: }
257:
258: $return = null;
259: $list = Set::normalize($list, false);
260:
261: if (array_key_exists($select, $list)) {
262: $return = $list[$select];
263: }
264: return $return;
265: }
266:
267: /**
268: * Returns a series of values extracted from an array, formatted in a format string.
269: *
270: * @param array $data Source array from which to extract the data
271: * @param string $format Format string into which values will be inserted, see sprintf()
272: * @param array $keys An array containing one or more Set::extract()-style key paths
273: * @return array An array of strings extracted from $keys and formatted with $format
274: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::format
275: */
276: public static function format($data, $format, $keys) {
277: $extracted = array();
278: $count = count($keys);
279:
280: if (!$count) {
281: return;
282: }
283:
284: for ($i = 0; $i < $count; $i++) {
285: $extracted[] = Set::extract($data, $keys[$i]);
286: }
287: $out = array();
288: $data = $extracted;
289: $count = count($data[0]);
290:
291: if (preg_match_all('/\{([0-9]+)\}/msi', $format, $keys2) && isset($keys2[1])) {
292: $keys = $keys2[1];
293: $format = preg_split('/\{([0-9]+)\}/msi', $format);
294: $count2 = count($format);
295:
296: for ($j = 0; $j < $count; $j++) {
297: $formatted = '';
298: for ($i = 0; $i <= $count2; $i++) {
299: if (isset($format[$i])) {
300: $formatted .= $format[$i];
301: }
302: if (isset($keys[$i]) && isset($data[$keys[$i]][$j])) {
303: $formatted .= $data[$keys[$i]][$j];
304: }
305: }
306: $out[] = $formatted;
307: }
308: } else {
309: $count2 = count($data);
310: for ($j = 0; $j < $count; $j++) {
311: $args = array();
312: for ($i = 0; $i < $count2; $i++) {
313: if (array_key_exists($j, $data[$i])) {
314: $args[] = $data[$i][$j];
315: }
316: }
317: $out[] = vsprintf($format, $args);
318: }
319: }
320: return $out;
321: }
322:
323: /**
324: * Implements partial support for XPath 2.0. If $path does not contain a '/' the call
325: * is delegated to Set::classicExtract(). Also the $path and $data arguments are
326: * reversible.
327: *
328: * #### Currently implemented selectors:
329: *
330: * - /User/id (similar to the classic {n}.User.id)
331: * - /User[2]/name (selects the name of the second User)
332: * - /User[id>2] (selects all Users with an id > 2)
333: * - /User[id>2][<5] (selects all Users with an id > 2 but < 5)
334: * - /Post/Comment[author_name=john]/../name (Selects the name of all Posts that have at least one Comment written by john)
335: * - /Posts[name] (Selects all Posts that have a 'name' key)
336: * - /Comment/.[1] (Selects the contents of the first comment)
337: * - /Comment/.[:last] (Selects the last comment)
338: * - /Comment/.[:first] (Selects the first comment)
339: * - /Comment[text=/cakephp/i] (Selects the all comments that have a text matching the regex /cakephp/i)
340: * - /Comment/@* (Selects the all key names of all comments)
341: *
342: * #### Other limitations:
343: *
344: * - Only absolute paths starting with a single '/' are supported right now
345: *
346: * **Warning**: Even so it has plenty of unit tests the XPath support has not gone through a lot of
347: * real-world testing. Please report Bugs as you find them. Suggestions for additional features to
348: * implement are also very welcome!
349: *
350: * @param string $path An absolute XPath 2.0 path
351: * @param array $data An array of data to extract from
352: * @param array $options Currently only supports 'flatten' which can be disabled for higher XPath-ness
353: * @return array An array of matched items
354: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::extract
355: */
356: public static function extract($path, $data = null, $options = array()) {
357: if (is_string($data)) {
358: $tmp = $data;
359: $data = $path;
360: $path = $tmp;
361: }
362: if (strpos($path, '/') === false) {
363: return Set::classicExtract($data, $path);
364: }
365: if (empty($data)) {
366: return array();
367: }
368: if ($path === '/') {
369: return $data;
370: }
371: $contexts = $data;
372: $options = array_merge(array('flatten' => true), $options);
373: if (!isset($contexts[0])) {
374: $current = current($data);
375: if ((is_array($current) && count($data) < 1) || !is_array($current) || !Set::numeric(array_keys($data))) {
376: $contexts = array($data);
377: }
378: }
379: $tokens = array_slice(preg_split('/(?<!=|\\\\)\/(?![a-z-\s]*\])/', $path), 1);
380:
381: do {
382: $token = array_shift($tokens);
383: $conditions = false;
384: if (preg_match_all('/\[([^=]+=\/[^\/]+\/|[^\]]+)\]/', $token, $m)) {
385: $conditions = $m[1];
386: $token = substr($token, 0, strpos($token, '['));
387: }
388: $matches = array();
389: foreach ($contexts as $key => $context) {
390: if (!isset($context['trace'])) {
391: $context = array('trace' => array(null), 'item' => $context, 'key' => $key);
392: }
393: if ($token === '..') {
394: if (count($context['trace']) == 1) {
395: $context['trace'][] = $context['key'];
396: }
397: $parent = implode('/', $context['trace']) . '/.';
398: $context['item'] = Set::extract($parent, $data);
399: $context['key'] = array_pop($context['trace']);
400: if (isset($context['trace'][1]) && $context['trace'][1] > 0) {
401: $context['item'] = $context['item'][0];
402: } elseif (!empty($context['item'][$key])) {
403: $context['item'] = $context['item'][$key];
404: } else {
405: $context['item'] = array_shift($context['item']);
406: }
407: $matches[] = $context;
408: continue;
409: }
410: if ($token === '@*' && is_array($context['item'])) {
411: $matches[] = array(
412: 'trace' => array_merge($context['trace'], (array)$key),
413: 'key' => $key,
414: 'item' => array_keys($context['item']),
415: );
416: } elseif (is_array($context['item'])
417: && array_key_exists($token, $context['item'])
418: && !(strval($key) === strval($token) && count($tokens) == 1 && $tokens[0] === '.')) {
419: $items = $context['item'][$token];
420: if (!is_array($items)) {
421: $items = array($items);
422: } elseif (!isset($items[0])) {
423: $current = current($items);
424: $currentKey = key($items);
425: if (!is_array($current) || (is_array($current) && count($items) <= 1 && !is_numeric($currentKey))) {
426: $items = array($items);
427: }
428: }
429:
430: foreach ($items as $key => $item) {
431: $ctext = array($context['key']);
432: if (!is_numeric($key)) {
433: $ctext[] = $token;
434: $tok = array_shift($tokens);
435: if (isset($items[$tok])) {
436: $ctext[] = $tok;
437: $item = $items[$tok];
438: $matches[] = array(
439: 'trace' => array_merge($context['trace'], $ctext),
440: 'key' => $tok,
441: 'item' => $item,
442: );
443: break;
444: } elseif ($tok !== null) {
445: array_unshift($tokens, $tok);
446: }
447: } else {
448: $key = $token;
449: }
450:
451: $matches[] = array(
452: 'trace' => array_merge($context['trace'], $ctext),
453: 'key' => $key,
454: 'item' => $item,
455: );
456: }
457: } elseif ($key === $token || (ctype_digit($token) && $key == $token) || $token === '.') {
458: $context['trace'][] = $key;
459: $matches[] = array(
460: 'trace' => $context['trace'],
461: 'key' => $key,
462: 'item' => $context['item'],
463: );
464: }
465: }
466: if ($conditions) {
467: foreach ($conditions as $condition) {
468: $filtered = array();
469: $length = count($matches);
470: foreach ($matches as $i => $match) {
471: if (Set::matches(array($condition), $match['item'], $i + 1, $length)) {
472: $filtered[$i] = $match;
473: }
474: }
475: $matches = $filtered;
476: }
477: }
478: $contexts = $matches;
479:
480: if (empty($tokens)) {
481: break;
482: }
483: } while (1);
484:
485: $r = array();
486:
487: foreach ($matches as $match) {
488: if ((!$options['flatten'] || is_array($match['item'])) && !is_int($match['key'])) {
489: $r[] = array($match['key'] => $match['item']);
490: } else {
491: $r[] = $match['item'];
492: }
493: }
494: return $r;
495: }
496:
497: /**
498: * This function can be used to see if a single item or a given xpath match certain conditions.
499: *
500: * @param mixed $conditions An array of condition strings or an XPath expression
501: * @param array $data An array of data to execute the match on
502: * @param integer $i Optional: The 'nth'-number of the item being matched.
503: * @param integer $length
504: * @return boolean
505: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::matches
506: */
507: public static function matches($conditions, $data = array(), $i = null, $length = null) {
508: if (empty($conditions)) {
509: return true;
510: }
511: if (is_string($conditions)) {
512: return !!Set::extract($conditions, $data);
513: }
514: foreach ($conditions as $condition) {
515: if ($condition === ':last') {
516: if ($i != $length) {
517: return false;
518: }
519: continue;
520: } elseif ($condition === ':first') {
521: if ($i != 1) {
522: return false;
523: }
524: continue;
525: }
526: if (!preg_match('/(.+?)([><!]?[=]|[><])(.*)/', $condition, $match)) {
527: if (ctype_digit($condition)) {
528: if ($i != $condition) {
529: return false;
530: }
531: } elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) {
532: return in_array($i, $matches[0]);
533: } elseif (!array_key_exists($condition, $data)) {
534: return false;
535: }
536: continue;
537: }
538: list(, $key, $op, $expected) = $match;
539: if (!(isset($data[$key]) || array_key_exists($key, $data))) {
540: return false;
541: }
542:
543: $val = $data[$key];
544:
545: if ($op === '=' && $expected && $expected{0} === '/') {
546: return preg_match($expected, $val);
547: }
548: if ($op === '=' && $val != $expected) {
549: return false;
550: }
551: if ($op === '!=' && $val == $expected) {
552: return false;
553: }
554: if ($op === '>' && $val <= $expected) {
555: return false;
556: }
557: if ($op === '<' && $val >= $expected) {
558: return false;
559: }
560: if ($op === '<=' && $val > $expected) {
561: return false;
562: }
563: if ($op === '>=' && $val < $expected) {
564: return false;
565: }
566: }
567: return true;
568: }
569:
570: /**
571: * Gets a value from an array or object that is contained in a given path using an array path syntax, i.e.:
572: * "{n}.Person.{[a-z]+}" - Where "{n}" represents a numeric key, "Person" represents a string literal,
573: * and "{[a-z]+}" (i.e. any string literal enclosed in brackets besides {n} and {s}) is interpreted as
574: * a regular expression.
575: *
576: * @param array $data Array from where to extract
577: * @param mixed $path As an array, or as a dot-separated string.
578: * @return array Extracted data
579: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::classicExtract
580: */
581: public static function classicExtract($data, $path = null) {
582: if (empty($path)) {
583: return $data;
584: }
585: if (is_object($data)) {
586: if (!($data instanceof ArrayAccess || $data instanceof Traversable)) {
587: $data = get_object_vars($data);
588: }
589: }
590: if (empty($data)) {
591: return null;
592: }
593: if (is_string($path) && strpos($path, '{') !== false) {
594: $path = String::tokenize($path, '.', '{', '}');
595: } elseif (is_string($path)) {
596: $path = explode('.', $path);
597: }
598: $tmp = array();
599:
600: if (empty($path)) {
601: return null;
602: }
603:
604: foreach ($path as $i => $key) {
605: if (is_numeric($key) && intval($key) > 0 || $key === '0') {
606: if (isset($data[$key])) {
607: $data = $data[$key];
608: } else {
609: return null;
610: }
611: } elseif ($key === '{n}') {
612: foreach ($data as $j => $val) {
613: if (is_int($j)) {
614: $tmpPath = array_slice($path, $i + 1);
615: if (empty($tmpPath)) {
616: $tmp[] = $val;
617: } else {
618: $tmp[] = Set::classicExtract($val, $tmpPath);
619: }
620: }
621: }
622: return $tmp;
623: } elseif ($key === '{s}') {
624: foreach ($data as $j => $val) {
625: if (is_string($j)) {
626: $tmpPath = array_slice($path, $i + 1);
627: if (empty($tmpPath)) {
628: $tmp[] = $val;
629: } else {
630: $tmp[] = Set::classicExtract($val, $tmpPath);
631: }
632: }
633: }
634: return $tmp;
635: } elseif (false !== strpos($key, '{') && false !== strpos($key, '}')) {
636: $pattern = substr($key, 1, -1);
637:
638: foreach ($data as $j => $val) {
639: if (preg_match('/^' . $pattern . '/s', $j) !== 0) {
640: $tmpPath = array_slice($path, $i + 1);
641: if (empty($tmpPath)) {
642: $tmp[$j] = $val;
643: } else {
644: $tmp[$j] = Set::classicExtract($val, $tmpPath);
645: }
646: }
647: }
648: return $tmp;
649: } else {
650: if (isset($data[$key])) {
651: $data = $data[$key];
652: } else {
653: return null;
654: }
655: }
656: }
657: return $data;
658: }
659:
660: /**
661: * Inserts $data into an array as defined by $path.
662: *
663: * @param mixed $list Where to insert into
664: * @param mixed $path A dot-separated string.
665: * @param array $data Data to insert
666: * @return array
667: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::insert
668: */
669: public static function insert($list, $path, $data = null) {
670: if (!is_array($path)) {
671: $path = explode('.', $path);
672: }
673: $_list =& $list;
674:
675: $count = count($path);
676: foreach ($path as $i => $key) {
677: if (is_numeric($key) && intval($key) > 0 || $key === '0') {
678: $key = intval($key);
679: }
680: if ($i === $count - 1 && is_array($_list)) {
681: $_list[$key] = $data;
682: } else {
683: if (!isset($_list[$key])) {
684: $_list[$key] = array();
685: }
686: $_list =& $_list[$key];
687: }
688: if (!is_array($_list)) {
689: $_list = array();
690: }
691: }
692: return $list;
693: }
694:
695: /**
696: * Removes an element from a Set or array as defined by $path.
697: *
698: * @param mixed $list From where to remove
699: * @param mixed $path A dot-separated string.
700: * @return array Array with $path removed from its value
701: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::remove
702: */
703: public static function remove($list, $path = null) {
704: if (empty($path)) {
705: return $list;
706: }
707: if (!is_array($path)) {
708: $path = explode('.', $path);
709: }
710: $_list =& $list;
711:
712: foreach ($path as $i => $key) {
713: if (is_numeric($key) && intval($key) > 0 || $key === '0') {
714: $key = intval($key);
715: }
716: if ($i === count($path) - 1) {
717: unset($_list[$key]);
718: } else {
719: if (!isset($_list[$key])) {
720: return $list;
721: }
722: $_list =& $_list[$key];
723: }
724: }
725: return $list;
726: }
727:
728: /**
729: * Checks if a particular path is set in an array
730: *
731: * @param mixed $data Data to check on
732: * @param mixed $path A dot-separated string.
733: * @return boolean true if path is found, false otherwise
734: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::check
735: */
736: public static function check($data, $path = null) {
737: if (empty($path)) {
738: return $data;
739: }
740: if (!is_array($path)) {
741: $path = explode('.', $path);
742: }
743:
744: foreach ($path as $i => $key) {
745: if (is_numeric($key) && intval($key) > 0 || $key === '0') {
746: $key = intval($key);
747: }
748: if ($i === count($path) - 1) {
749: return (is_array($data) && array_key_exists($key, $data));
750: }
751:
752: if (!is_array($data) || !array_key_exists($key, $data)) {
753: return false;
754: }
755: $data =& $data[$key];
756: }
757: return true;
758: }
759:
760: /**
761: * Computes the difference between a Set and an array, two Sets, or two arrays
762: *
763: * @param mixed $val1 First value
764: * @param mixed $val2 Second value
765: * @return array Returns the key => value pairs that are not common in $val1 and $val2
766: * The expression for this function is($val1 - $val2) + ($val2 - ($val1 - $val2))
767: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::diff
768: */
769: public static function diff($val1, $val2 = null) {
770: if (empty($val1)) {
771: return (array)$val2;
772: }
773: if (empty($val2)) {
774: return (array)$val1;
775: }
776: $intersection = array_intersect_key($val1, $val2);
777: while (($key = key($intersection)) !== null) {
778: if ($val1[$key] == $val2[$key]) {
779: unset($val1[$key]);
780: unset($val2[$key]);
781: }
782: next($intersection);
783: }
784:
785: return $val1 + $val2;
786: }
787:
788: /**
789: * Determines if one Set or array contains the exact keys and values of another.
790: *
791: * @param array $val1 First value
792: * @param array $val2 Second value
793: * @return boolean true if $val1 contains $val2, false otherwise
794: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::contains
795: */
796: public static function contains($val1, $val2 = null) {
797: if (empty($val1) || empty($val2)) {
798: return false;
799: }
800:
801: foreach ($val2 as $key => $val) {
802: if (is_numeric($key)) {
803: Set::contains($val, $val1);
804: } else {
805: if (!isset($val1[$key]) || $val1[$key] != $val) {
806: return false;
807: }
808: }
809: }
810: return true;
811: }
812:
813: /**
814: * Counts the dimensions of an array. If $all is set to false (which is the default) it will
815: * only consider the dimension of the first element in the array.
816: *
817: * @param array $array Array to count dimensions on
818: * @param boolean $all Set to true to count the dimension considering all elements in array
819: * @param integer $count Start the dimension count at this number
820: * @return integer The number of dimensions in $array
821: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::countDim
822: */
823: public static function countDim($array = null, $all = false, $count = 0) {
824: if ($all) {
825: $depth = array($count);
826: if (is_array($array) && reset($array) !== false) {
827: foreach ($array as $value) {
828: $depth[] = Set::countDim($value, true, $count + 1);
829: }
830: }
831: $return = max($depth);
832: } else {
833: if (is_array(reset($array))) {
834: $return = Set::countDim(reset($array)) + 1;
835: } else {
836: $return = 1;
837: }
838: }
839: return $return;
840: }
841:
842: /**
843: * Normalizes a string or array list.
844: *
845: * @param mixed $list List to normalize
846: * @param boolean $assoc If true, $list will be converted to an associative array
847: * @param string $sep If $list is a string, it will be split into an array with $sep
848: * @param boolean $trim If true, separated strings will be trimmed
849: * @return array
850: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::normalize
851: */
852: public static function normalize($list, $assoc = true, $sep = ',', $trim = true) {
853: if (is_string($list)) {
854: $list = explode($sep, $list);
855: if ($trim) {
856: foreach ($list as $key => $value) {
857: $list[$key] = trim($value);
858: }
859: }
860: if ($assoc) {
861: return Set::normalize($list);
862: }
863: } elseif (is_array($list)) {
864: $keys = array_keys($list);
865: $count = count($keys);
866: $numeric = true;
867:
868: if (!$assoc) {
869: for ($i = 0; $i < $count; $i++) {
870: if (!is_int($keys[$i])) {
871: $numeric = false;
872: break;
873: }
874: }
875: }
876: if (!$numeric || $assoc) {
877: $newList = array();
878: for ($i = 0; $i < $count; $i++) {
879: if (is_int($keys[$i])) {
880: $newList[$list[$keys[$i]]] = null;
881: } else {
882: $newList[$keys[$i]] = $list[$keys[$i]];
883: }
884: }
885: $list = $newList;
886: }
887: }
888: return $list;
889: }
890:
891: /**
892: * Creates an associative array using a $path1 as the path to build its keys, and optionally
893: * $path2 as path to get the values. If $path2 is not specified, all values will be initialized
894: * to null (useful for Set::merge). You can optionally group the values by what is obtained when
895: * following the path specified in $groupPath.
896: *
897: * @param mixed $data Array or object from where to extract keys and values
898: * @param mixed $path1 As an array, or as a dot-separated string.
899: * @param mixed $path2 As an array, or as a dot-separated string.
900: * @param string $groupPath As an array, or as a dot-separated string.
901: * @return array Combined array
902: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::combine
903: */
904: public static function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
905: if (empty($data)) {
906: return array();
907: }
908:
909: if (is_object($data)) {
910: if (!($data instanceof ArrayAccess || $data instanceof Traversable)) {
911: $data = get_object_vars($data);
912: }
913: }
914:
915: if (is_array($path1)) {
916: $format = array_shift($path1);
917: $keys = Set::format($data, $format, $path1);
918: } else {
919: $keys = Set::extract($data, $path1);
920: }
921: if (empty($keys)) {
922: return array();
923: }
924:
925: if (!empty($path2) && is_array($path2)) {
926: $format = array_shift($path2);
927: $vals = Set::format($data, $format, $path2);
928: } elseif (!empty($path2)) {
929: $vals = Set::extract($data, $path2);
930: } else {
931: $count = count($keys);
932: for ($i = 0; $i < $count; $i++) {
933: $vals[$i] = null;
934: }
935: }
936:
937: if ($groupPath != null) {
938: $group = Set::extract($data, $groupPath);
939: if (!empty($group)) {
940: $c = count($keys);
941: for ($i = 0; $i < $c; $i++) {
942: if (!isset($group[$i])) {
943: $group[$i] = 0;
944: }
945: if (!isset($out[$group[$i]])) {
946: $out[$group[$i]] = array();
947: }
948: $out[$group[$i]][$keys[$i]] = $vals[$i];
949: }
950: return $out;
951: }
952: }
953: if (empty($vals)) {
954: return array();
955: }
956: return array_combine($keys, $vals);
957: }
958:
959: /**
960: * Converts an object into an array.
961: * @param object $object Object to reverse
962: * @return array Array representation of given object
963: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::reverse
964: */
965: public static function reverse($object) {
966: $out = array();
967: if ($object instanceof SimpleXMLElement) {
968: return Xml::toArray($object);
969: } elseif (is_object($object)) {
970: $keys = get_object_vars($object);
971: if (isset($keys['_name_'])) {
972: $identity = $keys['_name_'];
973: unset($keys['_name_']);
974: }
975: $new = array();
976: foreach ($keys as $key => $value) {
977: if (is_array($value)) {
978: $new[$key] = (array)Set::reverse($value);
979: } else {
980: // @codingStandardsIgnoreStart Legacy junk
981: if (isset($value->_name_)) {
982: $new = array_merge($new, Set::reverse($value));
983: } else {
984: $new[$key] = Set::reverse($value);
985: }
986: // @codingStandardsIgnoreEnd
987: }
988: }
989: if (isset($identity)) {
990: $out[$identity] = $new;
991: } else {
992: $out = $new;
993: }
994: } elseif (is_array($object)) {
995: foreach ($object as $key => $value) {
996: $out[$key] = Set::reverse($value);
997: }
998: } else {
999: $out = $object;
1000: }
1001: return $out;
1002: }
1003:
1004: /**
1005: * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
1006: * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
1007: * array('0.Foo.Bar' => 'Far').
1008: *
1009: * @param array $data Array to flatten
1010: * @param string $separator String used to separate array key elements in a path, defaults to '.'
1011: * @return array
1012: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::flatten
1013: */
1014: public static function flatten($data, $separator = '.') {
1015: $result = array();
1016: $path = null;
1017:
1018: if (is_array($separator)) {
1019: extract($separator, EXTR_OVERWRITE);
1020: }
1021:
1022: if (!is_null($path)) {
1023: $path .= $separator;
1024: }
1025:
1026: foreach ($data as $key => $val) {
1027: if (is_array($val)) {
1028: $result += (array)Set::flatten($val, array(
1029: 'separator' => $separator,
1030: 'path' => $path . $key
1031: ));
1032: } else {
1033: $result[$path . $key] = $val;
1034: }
1035: }
1036: return $result;
1037: }
1038:
1039: /**
1040: * Flattens an array for sorting
1041: *
1042: * @param array $results
1043: * @param string $key
1044: * @return array
1045: */
1046: protected static function _flatten($results, $key = null) {
1047: $stack = array();
1048: foreach ($results as $k => $r) {
1049: $id = $k;
1050: if (!is_null($key)) {
1051: $id = $key;
1052: }
1053: if (is_array($r) && !empty($r)) {
1054: $stack = array_merge($stack, Set::_flatten($r, $id));
1055: } else {
1056: $stack[] = array('id' => $id, 'value' => $r);
1057: }
1058: }
1059: return $stack;
1060: }
1061:
1062: /**
1063: * Sorts an array by any value, determined by a Set-compatible path
1064: *
1065: * @param array $data An array of data to sort
1066: * @param string $path A Set-compatible path to the array value
1067: * @param string $dir Direction of sorting - either ascending (ASC), or descending (DESC)
1068: * @return array Sorted array of data
1069: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::sort
1070: */
1071: public static function sort($data, $path, $dir) {
1072: $originalKeys = array_keys($data);
1073: $numeric = false;
1074: if (is_numeric(implode('', $originalKeys))) {
1075: $data = array_values($data);
1076: $numeric = true;
1077: }
1078: $result = Set::_flatten(Set::extract($data, $path));
1079: list($keys, $values) = array(Set::extract($result, '{n}.id'), Set::extract($result, '{n}.value'));
1080:
1081: $dir = strtolower($dir);
1082: if ($dir === 'asc') {
1083: $dir = SORT_ASC;
1084: } elseif ($dir === 'desc') {
1085: $dir = SORT_DESC;
1086: }
1087: array_multisort($values, $dir, $keys, $dir);
1088: $sorted = array();
1089: $keys = array_unique($keys);
1090:
1091: foreach ($keys as $k) {
1092: if ($numeric) {
1093: $sorted[] = $data[$k];
1094: } else {
1095: if (isset($originalKeys[$k])) {
1096: $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
1097: } else {
1098: $sorted[$k] = $data[$k];
1099: }
1100: }
1101: }
1102: return $sorted;
1103: }
1104:
1105: /**
1106: * Allows the application of a callback method to elements of an
1107: * array extracted by a Set::extract() compatible path.
1108: *
1109: * @param mixed $path Set-compatible path to the array value
1110: * @param array $data An array of data to extract from & then process with the $callback.
1111: * @param mixed $callback Callback method to be applied to extracted data.
1112: * See http://ca2.php.net/manual/en/language.pseudo-types.php#language.types.callback for examples
1113: * of callback formats.
1114: * @param array $options Options are:
1115: * - type : can be pass, map, or reduce. Map will handoff the given callback
1116: * to array_map, reduce will handoff to array_reduce, and pass will
1117: * use call_user_func_array().
1118: * @return mixed Result of the callback when applied to extracted data
1119: * @link http://book.cakephp.org/2.0/en/core-utility-libraries/set.html#Set::apply
1120: */
1121: public static function apply($path, $data, $callback, $options = array()) {
1122: $defaults = array('type' => 'pass');
1123: $options = array_merge($defaults, $options);
1124: $extracted = Set::extract($path, $data);
1125:
1126: if ($options['type'] === 'map') {
1127: return array_map($callback, $extracted);
1128: } elseif ($options['type'] === 'reduce') {
1129: return array_reduce($extracted, $callback);
1130: } elseif ($options['type'] === 'pass') {
1131: return call_user_func_array($callback, array($extracted));
1132: }
1133: return null;
1134: }
1135:
1136: /**
1137: * Takes in a flat array and returns a nested array
1138: *
1139: * @param mixed $data
1140: * @param array $options Options are:
1141: * children - the key name to use in the resultset for children
1142: * idPath - the path to a key that identifies each entry
1143: * parentPath - the path to a key that identifies the parent of each entry
1144: * root - the id of the desired top-most result
1145: * @return array of results, nested
1146: * @link
1147: */
1148: public static function nest($data, $options = array()) {
1149: if (!$data) {
1150: return $data;
1151: }
1152:
1153: $alias = key(current($data));
1154: $options += array(
1155: 'idPath' => "/$alias/id",
1156: 'parentPath' => "/$alias/parent_id",
1157: 'children' => 'children',
1158: 'root' => null
1159: );
1160:
1161: $return = $idMap = array();
1162: $ids = Set::extract($data, $options['idPath']);
1163: $idKeys = explode('/', trim($options['idPath'], '/'));
1164: $parentKeys = explode('/', trim($options['parentPath'], '/'));
1165:
1166: foreach ($data as $result) {
1167: $result[$options['children']] = array();
1168:
1169: $id = Set::get($result, $idKeys);
1170: $parentId = Set::get($result, $parentKeys);
1171:
1172: if (isset($idMap[$id][$options['children']])) {
1173: $idMap[$id] = array_merge($result, (array)$idMap[$id]);
1174: } else {
1175: $idMap[$id] = array_merge($result, array($options['children'] => array()));
1176: }
1177: if (!$parentId || !in_array($parentId, $ids)) {
1178: $return[] =& $idMap[$id];
1179: } else {
1180: $idMap[$parentId][$options['children']][] =& $idMap[$id];
1181: }
1182: }
1183:
1184: if ($options['root']) {
1185: $root = $options['root'];
1186: } else {
1187: $root = Set::get($return[0], $parentKeys);
1188: }
1189:
1190: foreach ($return as $i => $result) {
1191: $id = Set::get($result, $idKeys);
1192: $parentId = Set::get($result, $parentKeys);
1193: if ($id !== $root && $parentId != $root) {
1194: unset($return[$i]);
1195: }
1196: }
1197:
1198: return array_values($return);
1199: }
1200:
1201: /**
1202: * Return the value at the specified position
1203: *
1204: * @param mixed $input an array
1205: * @param mixed $path string or array of array keys
1206: * @return the value at the specified position or null if it doesn't exist
1207: */
1208: public static function get($input, $path = null) {
1209: if (is_string($path)) {
1210: if (strpos($path, '/') !== false) {
1211: $keys = explode('/', trim($path, '/'));
1212: } else {
1213: $keys = explode('.', trim($path, '.'));
1214: }
1215: } else {
1216: $keys = $path;
1217: }
1218: if (!$keys) {
1219: return $input;
1220: }
1221:
1222: $return = $input;
1223: foreach ($keys as $key) {
1224: if (!isset($return[$key])) {
1225: return null;
1226: }
1227: $return = $return[$key];
1228: }
1229: return $return;
1230: }
1231:
1232: }
1233: