Merge remote-tracking branch 'upstream/4.4' into 4.4-master-2014-04-03-23-46-36
[civicrm-core.git] / CRM / Utils / Array.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * Provides a collection of static methods for array manipulation.
30 *
31 * @package CRM
32 * @copyright CiviCRM LLC (c) 2004-2014
33 */
34 class CRM_Utils_Array {
35
36 /**
37 * Returns $list[$key] if such element exists, or a default value otherwise.
38 *
39 * If $list is not actually an array at all, then the default value is
40 * returned.
41 *
42 * @access public
43 *
44 * @param string $key
45 * Key value to look up in the array.
46 * @param array $list
47 * Array from which to look up a value.
48 * @param mixed $default
49 * (optional) Value to return $list[$key] does not exist.
50 *
51 * @return mixed
52 * Can return any type, since $list might contain anything.
53 */
54 static function value($key, $list, $default = NULL) {
55 if (is_array($list)) {
56 return array_key_exists($key, $list) ? $list[$key] : $default;
57 }
58 return $default;
59 }
60
61 /**
62 * Recursively searches an array for a key, returning the first value found.
63 *
64 * If $params[$key] does not exist and $params contains arrays, descend into
65 * each array in a depth-first manner, in array iteration order.
66 *
67 * @param array $params
68 * The array to be searched.
69 * @param string $key
70 * The key to search for.
71 *
72 * @return mixed
73 * The value of the key, or null if the key is not found.
74 * @access public
75 */
76 static function retrieveValueRecursive(&$params, $key) {
77 if (!is_array($params)) {
78 return NULL;
79 }
80 elseif ($value = CRM_Utils_Array::value($key, $params)) {
81 return $value;
82 }
83 else {
84 foreach ($params as $subParam) {
85 if (is_array($subParam) &&
86 $value = self::retrieveValueRecursive($subParam, $key)
87 ) {
88 return $value;
89 }
90 }
91 }
92 return NULL;
93 }
94
95 /**
96 * Wraps and slightly changes the behavior of PHP's array_search().
97 *
98 * This function reproduces the behavior of array_search() from PHP prior to
99 * version 4.2.0, which was to return NULL on failure. This function also
100 * checks that $list is an array before attempting to search it.
101 *
102 * @access public
103 *
104 * @param mixed $value
105 * The value to search for.
106 * @param array $list
107 * The array to be searched.
108 *
109 * @return int|string|null
110 * Returns the key, which could be an int or a string, or NULL on failure.
111 */
112 static function key($value, &$list) {
113 if (is_array($list)) {
114 $key = array_search($value, $list);
115
116 // array_search returns key if found, false otherwise
117 // it may return values like 0 or empty string which
118 // evaluates to false
119 // hence we must use identical comparison operator
120 return ($key === FALSE) ? NULL : $key;
121 }
122 return NULL;
123 }
124
125 /**
126 * Builds an XML fragment representing an array.
127 *
128 * Depending on the nature of the keys of the array (and its sub-arrays,
129 * if any) the XML fragment may not be valid.
130 *
131 * @param array $list
132 * The array to be serialized.
133 * @param int $depth
134 * (optional) Indentation depth counter.
135 * @param string $seperator
136 * (optional) String to be appended after open/close tags.
137 *
138 * @access public
139 *
140 * @return string
141 * XML fragment representing $list.
142 */
143 static function &xml(&$list, $depth = 1, $seperator = "\n") {
144 $xml = '';
145 foreach ($list as $name => $value) {
146 $xml .= str_repeat(' ', $depth * 4);
147 if (is_array($value)) {
148 $xml .= "<{$name}>{$seperator}";
149 $xml .= self::xml($value, $depth + 1, $seperator);
150 $xml .= str_repeat(' ', $depth * 4);
151 $xml .= "</{$name}>{$seperator}";
152 }
153 else {
154 // make sure we escape value
155 $value = self::escapeXML($value);
156 $xml .= "<{$name}>$value</{$name}>{$seperator}";
157 }
158 }
159 return $xml;
160 }
161
162 /**
163 * Sanitizes a string for serialization in CRM_Utils_Array::xml().
164 *
165 * Replaces '&', '<', and '>' with their XML escape sequences. Replaces '^A'
166 * with a comma.
167 *
168 * @param string $value
169 * String to be sanitized.
170 *
171 * @return string
172 * Sanitized version of $value.
173 */
174 static function escapeXML($value) {
175 static $src = NULL;
176 static $dst = NULL;
177
178 if (!$src) {
179 $src = array('&', '<', '>', '\ 1');
180 $dst = array('&amp;', '&lt;', '&gt;', ',');
181 }
182
183 return str_replace($src, $dst, $value);
184 }
185
186 /**
187 * Converts a nested array to a flat array.
188 *
189 * The nested structure is preserved in the string values of the keys of the
190 * flat array.
191 *
192 * Example nested array:
193 * Array
194 * (
195 * [foo] => Array
196 * (
197 * [0] => bar
198 * [1] => baz
199 * [2] => 42
200 * )
201 *
202 * [asdf] => Array
203 * (
204 * [merp] => bleep
205 * [quack] => Array
206 * (
207 * [0] => 1
208 * [1] => 2
209 * [2] => 3
210 * )
211 *
212 * )
213 *
214 * [quux] => 999
215 * )
216 *
217 * Corresponding flattened array:
218 * Array
219 * (
220 * [foo.0] => bar
221 * [foo.1] => baz
222 * [foo.2] => 42
223 * [asdf.merp] => bleep
224 * [asdf.quack.0] => 1
225 * [asdf.quack.1] => 2
226 * [asdf.quack.2] => 3
227 * [quux] => 999
228 * )
229 *
230 * @param array $list
231 * Array to be flattened.
232 * @param array $flat
233 * Destination array.
234 * @param string $prefix
235 * (optional) String to prepend to keys.
236 * @param string $seperator
237 * (optional) String that separates the concatenated keys.
238 *
239 * @access public
240 */
241 static function flatten(&$list, &$flat, $prefix = '', $seperator = ".") {
242 foreach ($list as $name => $value) {
243 $newPrefix = ($prefix) ? $prefix . $seperator . $name : $name;
244 if (is_array($value)) {
245 self::flatten($value, $flat, $newPrefix, $seperator);
246 }
247 else {
248 if (!empty($value)) {
249 $flat[$newPrefix] = $value;
250 }
251 }
252 }
253 }
254
255 /**
256 * Converts an array with path-like keys into a tree of arrays.
257 *
258 * This function is the inverse of CRM_Utils_Array::flatten().
259 *
260 * @param string $delim
261 * A path delimiter
262 * @param array $arr
263 * A one-dimensional array indexed by string keys
264 *
265 * @return array
266 * Array-encoded tree
267 *
268 * @access public
269 */
270 function unflatten($delim, &$arr) {
271 $result = array();
272 foreach ($arr as $key => $value) {
273 $path = explode($delim, $key);
274 $node = &$result;
275 while (count($path) > 1) {
276 $key = array_shift($path);
277 if (!isset($node[$key])) {
278 $node[$key] = array();
279 }
280 $node = &$node[$key];
281 }
282 // last part of path
283 $key = array_shift($path);
284 $node[$key] = $value;
285 }
286 return $result;
287 }
288
289 /**
290 * Merges two arrays.
291 *
292 * If $a1[foo] and $a2[foo] both exist and are both arrays, the merge
293 * process recurses into those sub-arrays. If $a1[foo] and $a2[foo] both
294 * exist but they are not both arrays, the value from $a1 overrides the
295 * value from $a2 and the value from $a2 is discarded.
296 *
297 * @param array $a1
298 * First array to be merged.
299 * @param array $a2
300 * Second array to be merged.
301 *
302 * @return array
303 * The merged array.
304 * @access public
305 */
306 static function crmArrayMerge($a1, $a2) {
307 if (empty($a1)) {
308 return $a2;
309 }
310
311 if (empty($a2)) {
312 return $a1;
313 }
314
315 $a3 = array();
316 foreach ($a1 as $key => $value) {
317 if (array_key_exists($key, $a2) &&
318 is_array($a2[$key]) && is_array($a1[$key])
319 ) {
320 $a3[$key] = array_merge($a1[$key], $a2[$key]);
321 }
322 else {
323 $a3[$key] = $a1[$key];
324 }
325 }
326
327 foreach ($a2 as $key => $value) {
328 if (array_key_exists($key, $a1)) {
329 // already handled in above loop
330 continue;
331 }
332 $a3[$key] = $a2[$key];
333 }
334
335 return $a3;
336 }
337
338 /**
339 * Determines whether an array contains any sub-arrays.
340 *
341 * @param array $list
342 * The array to inspect.
343 *
344 * @return bool
345 * True if $list contains at least one sub-array, false otherwise.
346 * @access public
347 */
348 static function isHierarchical(&$list) {
349 foreach ($list as $n => $v) {
350 if (is_array($v)) {
351 return TRUE;
352 }
353 }
354 return FALSE;
355 }
356
357 /**
358 * Recursively copies all values of an array into a new array.
359 *
360 * If the recursion depth limit is exceeded, the deep copy appears to
361 * succeed, but the copy process past the depth limit will be shallow.
362 *
363 * @params array $array
364 * The array to copy.
365 * @params int $maxdepth
366 * (optional) Recursion depth limit.
367 * @params int $depth
368 * (optional) Current recursion depth.
369 *
370 * @return array
371 * The new copy of $array.
372 *
373 * @access public
374 */
375 static function array_deep_copy(&$array, $maxdepth = 50, $depth = 0) {
376 if ($depth > $maxdepth) {
377 return $array;
378 }
379 $copy = array();
380 foreach ($array as $key => $value) {
381 if (is_array($value)) {
382 array_deep_copy($value, $copy[$key], $maxdepth, ++$depth);
383 }
384 else {
385 $copy[$key] = $value;
386 }
387 }
388 return $copy;
389 }
390
391 /**
392 * Makes a shallow copy of a variable, returning the copy by value.
393 *
394 * In some cases, functions return an array by reference, but we really don't
395 * want to receive a reference.
396 *
397 * @param $array mixed
398 * Something to return a copy of.
399 * @return mixed
400 * The copy.
401 * @access public
402 */
403 static function breakReference($array) {
404 $copy = $array;
405 return $copy;
406 }
407
408 /**
409 * Removes a portion of an array.
410 *
411 * This function is similar to PHP's array_splice(), with some differences:
412 * - Array keys that are not removed are preserved. The PHP built-in
413 * function only preserves values.
414 * - The portion of the array to remove is specified by start and end
415 * index rather than offset and length.
416 * - There is no ability to specify data to replace the removed portion.
417 *
418 * The behavior given an associative array would probably not be useful.
419 *
420 * @param array $params
421 * Array to manipulate.
422 * @param int $start
423 * First index to remove.
424 * @param int $end
425 * Last index to remove.
426 *
427 * @access public
428 */
429 static function crmArraySplice(&$params, $start, $end) {
430 // verify start and end index
431 if ($start < 0) {
432 $start = 0;
433 }
434 if ($end > count($params)) {
435 $end = count($params);
436 }
437
438 $i = 0;
439
440 // procees unset operation
441 foreach ($params as $key => $value) {
442 if ($i >= $start && $i < $end) {
443 unset($params[$key]);
444 }
445 $i++;
446 }
447 }
448
449 /**
450 * Searches an array recursively in an optionally case-insensitive manner.
451 *
452 * @param string $value
453 * Value to search for.
454 * @param array $params
455 * Array to search within.
456 * @param bool $caseInsensitive
457 * (optional) Whether to search in a case-insensitive manner.
458 *
459 * @return bool
460 * True if $value was found, false otherwise.
461 *
462 * @access public
463 */
464 static function crmInArray($value, $params, $caseInsensitive = TRUE) {
465 foreach ($params as $item) {
466 if (is_array($item)) {
467 $ret = crmInArray($value, $item, $caseInsensitive);
468 }
469 else {
470 $ret = ($caseInsensitive) ? strtolower($item) == strtolower($value) : $item == $value;
471 if ($ret) {
472 return $ret;
473 }
474 }
475 }
476 return FALSE;
477 }
478
479 /**
480 * This function is used to convert associative array names to values
481 * and vice-versa.
482 *
483 * This function is used by both the web form layer and the api. Note that
484 * the api needs the name => value conversion, also the view layer typically
485 * requires value => name conversion
486 */
487 static function lookupValue(&$defaults, $property, $lookup, $reverse) {
488 $id = $property . '_id';
489
490 $src = $reverse ? $property : $id;
491 $dst = $reverse ? $id : $property;
492
493 if (!array_key_exists(strtolower($src), array_change_key_case($defaults, CASE_LOWER))) {
494 return FALSE;
495 }
496
497 $look = $reverse ? array_flip($lookup) : $lookup;
498
499 //trim lookup array, ignore . ( fix for CRM-1514 ), eg for prefix/suffix make sure Dr. and Dr both are valid
500 $newLook = array();
501 foreach ($look as $k => $v) {
502 $newLook[trim($k, ".")] = $v;
503 }
504
505 $look = $newLook;
506
507 if (is_array($look)) {
508 if (!array_key_exists(trim(strtolower($defaults[strtolower($src)]), '.'), array_change_key_case($look, CASE_LOWER))) {
509 return FALSE;
510 }
511 }
512
513 $tempLook = array_change_key_case($look, CASE_LOWER);
514
515 $defaults[$dst] = $tempLook[trim(strtolower($defaults[strtolower($src)]), '.')];
516 return TRUE;
517 }
518
519 /**
520 * Checks whether an array is empty.
521 *
522 * An array is empty if its values consist only of NULL and empty sub-arrays.
523 * Containing a non-NULL value or non-empty array makes an array non-empty.
524 *
525 * If something other than an array is passed, it is considered to be empty.
526 *
527 * If nothing is passed at all, the default value provided is empty.
528 *
529 * @param array $array
530 * (optional) Array to be checked for emptiness.
531 *
532 * @return boolean
533 * True if the array is empty.
534 * @access public
535 */
536 static function crmIsEmptyArray($array = array()) {
537 if (!is_array($array)) {
538 return TRUE;
539 }
540 foreach ($array as $element) {
541 if (is_array($element)) {
542 if (!self::crmIsEmptyArray($element)) {
543 return FALSE;
544 }
545 }
546 elseif (isset($element)) {
547 return FALSE;
548 }
549 }
550 return TRUE;
551 }
552
553 /**
554 * Determines the maximum depth of nested arrays in a multidimensional array.
555 *
556 * The mechanism for determining depth will be confused if the array
557 * contains keys or values with the left brace '{' character. This will
558 * cause the depth to be over-reported.
559 *
560 * @param array $array
561 * The array to examine.
562 *
563 * @return integer
564 * The maximum nested array depth found.
565 *
566 * @access public
567 */
568 static function getLevelsArray($array) {
569 if (!is_array($array)) {
570 return 0;
571 }
572 $jsonString = json_encode($array);
573 $parts = explode("}", $jsonString);
574 $max = 0;
575 foreach ($parts as $part) {
576 $countLevels = substr_count($part, "{");
577 if ($countLevels > $max) {
578 $max = $countLevels;
579 }
580 }
581 return $max;
582 }
583
584 /**
585 * Sorts an associative array of arrays by an attribute using strnatcmp().
586 *
587 * @param array $array
588 * Array to be sorted.
589 * @param string $field
590 * Name of the attribute used for sorting.
591 *
592 * @return array
593 * Sorted array
594 */
595 static function crmArraySortByField($array, $field) {
596 $code = "return strnatcmp(\$a['$field'], \$b['$field']);";
597 uasort($array, create_function('$a,$b', $code));
598 return $array;
599 }
600
601 /**
602 * Recursively removes duplicate values from a multi-dimensional array.
603 *
604 * @param array $array
605 * The input array possibly containing duplicate values.
606 *
607 * @return array
608 * The input array with duplicate values removed.
609 */
610 static function crmArrayUnique($array) {
611 $result = array_map("unserialize", array_unique(array_map("serialize", $array)));
612 foreach ($result as $key => $value) {
613 if (is_array($value)) {
614 $result[$key] = self::crmArrayUnique($value);
615 }
616 }
617 return $result;
618 }
619
620 /**
621 * Sorts an array and maintains index association (with localization).
622 *
623 * Uses Collate from the PECL "intl" package, if available, for UTF-8
624 * sorting (e.g. list of countries). Otherwise calls PHP's asort().
625 *
626 * On Debian/Ubuntu: apt-get install php5-intl
627 *
628 * @param array $array
629 * (optional) Array to be sorted.
630 *
631 * @return array
632 * Sorted array.
633 */
634 static function asort($array = array()) {
635 $lcMessages = CRM_Utils_System::getUFLocale();
636
637 if ($lcMessages && $lcMessages != 'en_US' && class_exists('Collator')) {
638 $collator = new Collator($lcMessages . '.utf8');
639 $collator->asort($array);
640 }
641 else {
642 // This calls PHP's built-in asort().
643 asort($array);
644 }
645
646 return $array;
647 }
648
649 /**
650 * Unsets an arbitrary list of array elements from an associative array.
651 *
652 * @param array $items
653 * The array from which to remove items.
654 * @param string[]|string $key,...
655 * When passed a string, unsets $items[$key].
656 * When passed an array of strings, unsets $items[$k] for each string $k
657 * in the array.
658 */
659 static function remove(&$items) {
660 foreach (func_get_args() as $n => $key) {
661 // Skip argument 0 ($items) by testing $n for truth.
662 if ($n && is_array($key)) {
663 foreach($key as $k) {
664 unset($items[$k]);
665 }
666 }
667 elseif ($n) {
668 unset($items[$key]);
669 }
670 }
671 }
672
673 /**
674 * Builds an array-tree which indexes the records in an array.
675 *
676 * @param string[] $keys
677 * Properties by which to index.
678 * @param object|array $records
679 *
680 * @return array
681 * Multi-dimensional array, with one layer for each key.
682 */
683 static function index($keys, $records) {
684 $final_key = array_pop($keys);
685
686 $result = array();
687 foreach ($records as $record) {
688 $node = &$result;
689 foreach ($keys as $key) {
690 if (is_array($record)) {
691 $keyvalue = $record[$key];
692 } else {
693 $keyvalue = $record->{$key};
694 }
695 if (isset($node[$keyvalue]) && !is_array($node[$keyvalue])) {
696 $node[$keyvalue] = array();
697 }
698 $node = &$node[$keyvalue];
699 }
700 if (is_array($record)) {
701 $node[ $record[$final_key] ] = $record;
702 } else {
703 $node[ $record->{$final_key} ] = $record;
704 }
705 }
706 return $result;
707 }
708
709 /**
710 * Iterates over a list of records and returns the value of some property.
711 *
712 * @param string $prop
713 * Property to retrieve.
714 * @param array|object $records
715 * A list of records.
716 *
717 * @return array
718 * Keys are the original keys of $records; values are the $prop values.
719 */
720 static function collect($prop, $records) {
721 $result = array();
722 foreach ($records as $key => $record) {
723 if (is_object($record)) {
724 $result[$key] = $record->{$prop};
725 } else {
726 $result[$key] = $record[$prop];
727 }
728 }
729 return $result;
730 }
731
732 /**
733 * Generate a string representation of an array.
734 *
735 * @param array $pairs
736 * Array to stringify.
737 * @param string $l1Delim
738 * String to use to separate key/value pairs from one another.
739 * @param string $l2Delim
740 * String to use to separate keys from values within each key/value pair.
741 *
742 * @return string
743 * Generated string.
744 */
745 static function implodeKeyValue($l1Delim, $l2Delim, $pairs) {
746 $exprs = array();
747 foreach ($pairs as $key => $value) {
748 $exprs[] = $key . $l2Delim . $value;
749 }
750 return implode($l1Delim, $exprs);
751 }
752
753 /**
754 * Trims delimiters from a string and then splits it using explode().
755 *
756 * This method works mostly like PHP's built-in explode(), except that
757 * surrounding delimiters are trimmed before explode() is called.
758 *
759 * Also, if an array or NULL is passed as the $values parameter, the value is
760 * returned unmodified rather than being passed to explode().
761 *
762 * @param array|null|string $values
763 * The input string (or an array, or NULL).
764 * @param string $delim
765 * (optional) The boundary string.
766 *
767 * @return array|null
768 * An array of strings produced by explode(), or the unmodified input
769 * array, or NULL.
770 */
771 static function explodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
772 if ($values === NULL) {
773 return NULL;
774 }
775 // If we already have an array, no need to continue
776 if (is_array($values)) {
777 return $values;
778 }
779 return explode($delim, trim((string) $values, $delim));
780 }
781
782 /**
783 * Joins array elements with a string, adding surrounding delimiters.
784 *
785 * This method works mostly like PHP's built-in implode(), but the generated
786 * string is surrounded by delimiter characters. Also, if NULL is passed as
787 * the $values parameter, NULL is returned.
788 *
789 * @param mixed $values
790 * Array to be imploded. If a non-array is passed, it will be cast to an
791 * array.
792 * @param string $delim
793 * Delimiter to be used for implode() and which will surround the output
794 * string.
795 *
796 * @return string|NULL
797 * The generated string, or NULL if NULL was passed as $values parameter.
798 */
799 static function implodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
800 if ($values === NULL) {
801 return NULL;
802 }
803 // If we already have a string, strip $delim off the ends so it doesn't get added twice
804 if (is_string($values)) {
805 $values = trim($values, $delim);
806 }
807 return $delim . implode($delim, (array) $values) . $delim;
808 }
809
810 /**
811 * Modifies a key in an array while preserving the key order.
812 *
813 * By default when an element is added to an array, it is added to the end.
814 * This method allows for changing an existing key while preserving its
815 * position in the array.
816 *
817 * The array is both modified in-place and returned.
818 *
819 * @param array $elementArray
820 * Array to manipulate.
821 * @param string $oldKey
822 * Old key to be replaced.
823 * @param string $newKey
824 * Replacement key string.
825 *
826 * @throws Exception
827 * Throws a generic Exception if $oldKey is not found in $elementArray.
828 *
829 * @return array
830 * The manipulated array.
831 */
832 static function crmReplaceKey(&$elementArray, $oldKey, $newKey) {
833 $keys = array_keys($elementArray);
834 if (FALSE === $index = array_search($oldKey, $keys)) {
835 throw new Exception(sprintf('key "%s" does not exit', $oldKey));
836 }
837 $keys[$index] = $newKey;
838 $elementArray = array_combine($keys, array_values($elementArray));
839 return $elementArray;
840 }
841
842 /*
843 * Searches array keys by regex, returning the value of the first match.
844 *
845 * Given a regular expression and an array, this method searches the keys
846 * of the array using the regular expression. The first match is then used
847 * to index into the array, and the associated value is retrieved and
848 * returned. If no matches are found, or if something other than an array
849 * is passed, then a default value is returned. Unless otherwise specified,
850 * the default value is NULL.
851 *
852 * @param string $regexKey
853 * The regular expression to use when searching for matching keys.
854 * @param array $list
855 * The array whose keys will be searched.
856 * @param mixed $default
857 * (optional) The default value to return if the regex does not match an
858 * array key, or if something other than an array is passed.
859 *
860 * @return mixed
861 * The value found.
862 */
863 static function valueByRegexKey($regexKey, $list, $default = NULL) {
864 if (is_array($list) && $regexKey) {
865 $matches = preg_grep($regexKey, array_keys($list));
866 $key = reset($matches);
867 return ($key && array_key_exists($key, $list)) ? $list[$key] : $default;
868 }
869 return $default;
870 }
871
872 /**
873 * Generates the Cartesian product of zero or more vectors.
874 *
875 * @param array $dimensions
876 * List of dimensions to multiply.
877 * Each key is a dimension name; each value is a vector.
878 * @param array $template
879 * (optional) A base set of values included in every output.
880 *
881 * @return array
882 * Each item is a distinct combination of values from $dimensions.
883 *
884 * For example, the product of
885 * {
886 * fg => {red, blue},
887 * bg => {white, black}
888 * }
889 * would be
890 * {
891 * {fg => red, bg => white},
892 * {fg => red, bg => black},
893 * {fg => blue, bg => white},
894 * {fg => blue, bg => black}
895 * }
896 */
897 static function product($dimensions, $template = array()) {
898 if (empty($dimensions)) {
899 return array($template);
900 }
901
902 foreach ($dimensions as $key => $value) {
903 $firstKey = $key;
904 $firstValues = $value;
905 break;
906 }
907 unset($dimensions[$key]);
908
909 $results = array();
910 foreach ($firstValues as $firstValue) {
911 foreach (self::product($dimensions, $template) as $result) {
912 $result[$firstKey] = $firstValue;
913 $results[] = $result;
914 }
915 }
916
917 return $results;
918 }
919 }
920