Merge pull request #8980 from ergonlogic/dev/CRM-19308
[civicrm-core.git] / CRM / Utils / Array.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
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-2017
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 *
43 * @param string $key
44 * Key value to look up in the array.
45 * @param array $list
46 * Array from which to look up a value.
47 * @param mixed $default
48 * (optional) Value to return $list[$key] does not exist.
49 *
50 * @return mixed
51 * Can return any type, since $list might contain anything.
52 */
53 public static function value($key, $list, $default = NULL) {
54 if (is_array($list)) {
55 return array_key_exists($key, $list) ? $list[$key] : $default;
56 }
57 return $default;
58 }
59
60 /**
61 * Recursively searches an array for a key, returning the first value found.
62 *
63 * If $params[$key] does not exist and $params contains arrays, descend into
64 * each array in a depth-first manner, in array iteration order.
65 *
66 * @param array $params
67 * The array to be searched.
68 * @param string $key
69 * The key to search for.
70 *
71 * @return mixed
72 * The value of the key, or null if the key is not found.
73 */
74 public static function retrieveValueRecursive(&$params, $key) {
75 if (!is_array($params)) {
76 return NULL;
77 }
78 elseif ($value = CRM_Utils_Array::value($key, $params)) {
79 return $value;
80 }
81 else {
82 foreach ($params as $subParam) {
83 if (is_array($subParam) &&
84 $value = self::retrieveValueRecursive($subParam, $key)
85 ) {
86 return $value;
87 }
88 }
89 }
90 return NULL;
91 }
92
93 /**
94 * Wraps and slightly changes the behavior of PHP's array_search().
95 *
96 * This function reproduces the behavior of array_search() from PHP prior to
97 * version 4.2.0, which was to return NULL on failure. This function also
98 * checks that $list is an array before attempting to search it.
99 *
100 *
101 * @param mixed $value
102 * The value to search for.
103 * @param array $list
104 * The array to be searched.
105 *
106 * @return int|string|null
107 * Returns the key, which could be an int or a string, or NULL on failure.
108 */
109 public static function key($value, $list) {
110 if (is_array($list)) {
111 $key = array_search($value, $list);
112
113 // array_search returns key if found, false otherwise
114 // it may return values like 0 or empty string which
115 // evaluates to false
116 // hence we must use identical comparison operator
117 return ($key === FALSE) ? NULL : $key;
118 }
119 return NULL;
120 }
121
122 /**
123 * Builds an XML fragment representing an array.
124 *
125 * Depending on the nature of the keys of the array (and its sub-arrays,
126 * if any) the XML fragment may not be valid.
127 *
128 * @param array $list
129 * The array to be serialized.
130 * @param int $depth
131 * (optional) Indentation depth counter.
132 * @param string $seperator
133 * (optional) String to be appended after open/close tags.
134 *
135 * @return string
136 * XML fragment representing $list.
137 */
138 public static function &xml(&$list, $depth = 1, $seperator = "\n") {
139 $xml = '';
140 foreach ($list as $name => $value) {
141 $xml .= str_repeat(' ', $depth * 4);
142 if (is_array($value)) {
143 $xml .= "<{$name}>{$seperator}";
144 $xml .= self::xml($value, $depth + 1, $seperator);
145 $xml .= str_repeat(' ', $depth * 4);
146 $xml .= "</{$name}>{$seperator}";
147 }
148 else {
149 // make sure we escape value
150 $value = self::escapeXML($value);
151 $xml .= "<{$name}>$value</{$name}>{$seperator}";
152 }
153 }
154 return $xml;
155 }
156
157 /**
158 * Sanitizes a string for serialization in CRM_Utils_Array::xml().
159 *
160 * Replaces '&', '<', and '>' with their XML escape sequences. Replaces '^A'
161 * with a comma.
162 *
163 * @param string $value
164 * String to be sanitized.
165 *
166 * @return string
167 * Sanitized version of $value.
168 */
169 public static function escapeXML($value) {
170 static $src = NULL;
171 static $dst = NULL;
172
173 if (!$src) {
174 $src = array('&', '<', '>', '\ 1');
175 $dst = array('&amp;', '&lt;', '&gt;', ',');
176 }
177
178 return str_replace($src, $dst, $value);
179 }
180
181 /**
182 * Converts a nested array to a flat array.
183 *
184 * The nested structure is preserved in the string values of the keys of the
185 * flat array.
186 *
187 * Example nested array:
188 * Array
189 * (
190 * [foo] => Array
191 * (
192 * [0] => bar
193 * [1] => baz
194 * [2] => 42
195 * )
196 *
197 * [asdf] => Array
198 * (
199 * [merp] => bleep
200 * [quack] => Array
201 * (
202 * [0] => 1
203 * [1] => 2
204 * [2] => 3
205 * )
206 *
207 * )
208 *
209 * [quux] => 999
210 * )
211 *
212 * Corresponding flattened array:
213 * Array
214 * (
215 * [foo.0] => bar
216 * [foo.1] => baz
217 * [foo.2] => 42
218 * [asdf.merp] => bleep
219 * [asdf.quack.0] => 1
220 * [asdf.quack.1] => 2
221 * [asdf.quack.2] => 3
222 * [quux] => 999
223 * )
224 *
225 * @param array $list
226 * Array to be flattened.
227 * @param array $flat
228 * Destination array.
229 * @param string $prefix
230 * (optional) String to prepend to keys.
231 * @param string $seperator
232 * (optional) String that separates the concatenated keys.
233 */
234 public static function flatten(&$list, &$flat, $prefix = '', $seperator = ".") {
235 foreach ($list as $name => $value) {
236 $newPrefix = ($prefix) ? $prefix . $seperator . $name : $name;
237 if (is_array($value)) {
238 self::flatten($value, $flat, $newPrefix, $seperator);
239 }
240 else {
241 if (!empty($value)) {
242 $flat[$newPrefix] = $value;
243 }
244 }
245 }
246 }
247
248 /**
249 * Converts an array with path-like keys into a tree of arrays.
250 *
251 * This function is the inverse of CRM_Utils_Array::flatten().
252 *
253 * @param string $delim
254 * A path delimiter
255 * @param array $arr
256 * A one-dimensional array indexed by string keys
257 *
258 * @return array
259 * Array-encoded tree
260 */
261 public function unflatten($delim, &$arr) {
262 $result = array();
263 foreach ($arr as $key => $value) {
264 $path = explode($delim, $key);
265 $node = &$result;
266 while (count($path) > 1) {
267 $key = array_shift($path);
268 if (!isset($node[$key])) {
269 $node[$key] = array();
270 }
271 $node = &$node[$key];
272 }
273 // last part of path
274 $key = array_shift($path);
275 $node[$key] = $value;
276 }
277 return $result;
278 }
279
280 /**
281 * Merges two arrays.
282 *
283 * If $a1[foo] and $a2[foo] both exist and are both arrays, the merge
284 * process recurses into those sub-arrays. If $a1[foo] and $a2[foo] both
285 * exist but they are not both arrays, the value from $a1 overrides the
286 * value from $a2 and the value from $a2 is discarded.
287 *
288 * @param array $a1
289 * First array to be merged.
290 * @param array $a2
291 * Second array to be merged.
292 *
293 * @return array
294 * The merged array.
295 */
296 public static function crmArrayMerge($a1, $a2) {
297 if (empty($a1)) {
298 return $a2;
299 }
300
301 if (empty($a2)) {
302 return $a1;
303 }
304
305 $a3 = array();
306 foreach ($a1 as $key => $value) {
307 if (array_key_exists($key, $a2) &&
308 is_array($a2[$key]) && is_array($a1[$key])
309 ) {
310 $a3[$key] = array_merge($a1[$key], $a2[$key]);
311 }
312 else {
313 $a3[$key] = $a1[$key];
314 }
315 }
316
317 foreach ($a2 as $key => $value) {
318 if (array_key_exists($key, $a1)) {
319 // already handled in above loop
320 continue;
321 }
322 $a3[$key] = $a2[$key];
323 }
324
325 return $a3;
326 }
327
328 /**
329 * Determines whether an array contains any sub-arrays.
330 *
331 * @param array $list
332 * The array to inspect.
333 *
334 * @return bool
335 * True if $list contains at least one sub-array, false otherwise.
336 */
337 public static function isHierarchical(&$list) {
338 foreach ($list as $n => $v) {
339 if (is_array($v)) {
340 return TRUE;
341 }
342 }
343 return FALSE;
344 }
345
346 /**
347 * Is array A a subset of array B.
348 *
349 * @param array $subset
350 * @param array $superset
351 *
352 * @return bool
353 * TRUE if $subset is a subset of $superset
354 */
355 public static function isSubset($subset, $superset) {
356 foreach ($subset as $expected) {
357 if (!in_array($expected, $superset)) {
358 return FALSE;
359 }
360 }
361 return TRUE;
362 }
363
364 /**
365 * Searches an array recursively in an optionally case-insensitive manner.
366 *
367 * @param string $value
368 * Value to search for.
369 * @param array $params
370 * Array to search within.
371 * @param bool $caseInsensitive
372 * (optional) Whether to search in a case-insensitive manner.
373 *
374 * @return bool
375 * True if $value was found, false otherwise.
376 */
377 public static function crmInArray($value, $params, $caseInsensitive = TRUE) {
378 foreach ($params as $item) {
379 if (is_array($item)) {
380 $ret = crmInArray($value, $item, $caseInsensitive);
381 }
382 else {
383 $ret = ($caseInsensitive) ? strtolower($item) == strtolower($value) : $item == $value;
384 if ($ret) {
385 return $ret;
386 }
387 }
388 }
389 return FALSE;
390 }
391
392 /**
393 * Convert associative array names to values and vice-versa.
394 *
395 * This function is used by both the web form layer and the api. Note that
396 * the api needs the name => value conversion, also the view layer typically
397 * requires value => name conversion
398 *
399 * @param array $defaults
400 * @param string $property
401 * @param $lookup
402 * @param $reverse
403 *
404 * @return bool
405 */
406 public static function lookupValue(&$defaults, $property, $lookup, $reverse) {
407 $id = $property . '_id';
408
409 $src = $reverse ? $property : $id;
410 $dst = $reverse ? $id : $property;
411
412 if (!array_key_exists(strtolower($src), array_change_key_case($defaults, CASE_LOWER))) {
413 return FALSE;
414 }
415
416 $look = $reverse ? array_flip($lookup) : $lookup;
417
418 // trim lookup array, ignore . ( fix for CRM-1514 ), eg for prefix/suffix make sure Dr. and Dr both are valid
419 $newLook = array();
420 foreach ($look as $k => $v) {
421 $newLook[trim($k, ".")] = $v;
422 }
423
424 $look = $newLook;
425
426 if (is_array($look)) {
427 if (!array_key_exists(trim(strtolower($defaults[strtolower($src)]), '.'), array_change_key_case($look, CASE_LOWER))) {
428 return FALSE;
429 }
430 }
431
432 $tempLook = array_change_key_case($look, CASE_LOWER);
433
434 $defaults[$dst] = $tempLook[trim(strtolower($defaults[strtolower($src)]), '.')];
435 return TRUE;
436 }
437
438 /**
439 * Checks whether an array is empty.
440 *
441 * An array is empty if its values consist only of NULL and empty sub-arrays.
442 * Containing a non-NULL value or non-empty array makes an array non-empty.
443 *
444 * If something other than an array is passed, it is considered to be empty.
445 *
446 * If nothing is passed at all, the default value provided is empty.
447 *
448 * @param array $array
449 * (optional) Array to be checked for emptiness.
450 *
451 * @return bool
452 * True if the array is empty.
453 */
454 public static function crmIsEmptyArray($array = array()) {
455 if (!is_array($array)) {
456 return TRUE;
457 }
458 foreach ($array as $element) {
459 if (is_array($element)) {
460 if (!self::crmIsEmptyArray($element)) {
461 return FALSE;
462 }
463 }
464 elseif (isset($element)) {
465 return FALSE;
466 }
467 }
468 return TRUE;
469 }
470
471 /**
472 * Sorts an associative array of arrays by an attribute using strnatcmp().
473 *
474 * @param array $array
475 * Array to be sorted.
476 * @param string $field
477 * Name of the attribute used for sorting.
478 *
479 * @return array
480 * Sorted array
481 */
482 public static function crmArraySortByField($array, $field) {
483 $code = "return strnatcmp(\$a['$field'], \$b['$field']);";
484 uasort($array, create_function('$a,$b', $code));
485 return $array;
486 }
487
488 /**
489 * Recursively removes duplicate values from a multi-dimensional array.
490 *
491 * @param array $array
492 * The input array possibly containing duplicate values.
493 *
494 * @return array
495 * The input array with duplicate values removed.
496 */
497 public static function crmArrayUnique($array) {
498 $result = array_map("unserialize", array_unique(array_map("serialize", $array)));
499 foreach ($result as $key => $value) {
500 if (is_array($value)) {
501 $result[$key] = self::crmArrayUnique($value);
502 }
503 }
504 return $result;
505 }
506
507 /**
508 * Sorts an array and maintains index association (with localization).
509 *
510 * Uses Collate from the PECL "intl" package, if available, for UTF-8
511 * sorting (e.g. list of countries). Otherwise calls PHP's asort().
512 *
513 * On Debian/Ubuntu: apt-get install php5-intl
514 *
515 * @param array $array
516 * (optional) Array to be sorted.
517 *
518 * @return array
519 * Sorted array.
520 */
521 public static function asort($array = array()) {
522 $lcMessages = CRM_Utils_System::getUFLocale();
523
524 if ($lcMessages && $lcMessages != 'en_US' && class_exists('Collator')) {
525 $collator = new Collator($lcMessages . '.utf8');
526 $collator->asort($array);
527 }
528 else {
529 // This calls PHP's built-in asort().
530 asort($array);
531 }
532
533 return $array;
534 }
535
536 /**
537 * Unsets an arbitrary list of array elements from an associative array.
538 *
539 * @param array $items
540 * The array from which to remove items.
541 *
542 * Additional params:
543 * When passed a string, unsets $items[$key].
544 * When passed an array of strings, unsets $items[$k] for each string $k in the array.
545 */
546 public static function remove(&$items) {
547 foreach (func_get_args() as $n => $key) {
548 // Skip argument 0 ($items) by testing $n for truth.
549 if ($n && is_array($key)) {
550 foreach ($key as $k) {
551 unset($items[$k]);
552 }
553 }
554 elseif ($n) {
555 unset($items[$key]);
556 }
557 }
558 }
559
560 /**
561 * Builds an array-tree which indexes the records in an array.
562 *
563 * @param string[] $keys
564 * Properties by which to index.
565 * @param object|array $records
566 *
567 * @return array
568 * Multi-dimensional array, with one layer for each key.
569 */
570 public static function index($keys, $records) {
571 $final_key = array_pop($keys);
572
573 $result = array();
574 foreach ($records as $record) {
575 $node = &$result;
576 foreach ($keys as $key) {
577 if (is_array($record)) {
578 $keyvalue = isset($record[$key]) ? $record[$key] : NULL;
579 }
580 else {
581 $keyvalue = isset($record->{$key}) ? $record->{$key} : NULL;
582 }
583 if (isset($node[$keyvalue]) && !is_array($node[$keyvalue])) {
584 $node[$keyvalue] = array();
585 }
586 $node = &$node[$keyvalue];
587 }
588 if (is_array($record)) {
589 $node[$record[$final_key]] = $record;
590 }
591 else {
592 $node[$record->{$final_key}] = $record;
593 }
594 }
595 return $result;
596 }
597
598 /**
599 * Iterates over a list of records and returns the value of some property.
600 *
601 * @param string $prop
602 * Property to retrieve.
603 * @param array|object $records
604 * A list of records.
605 *
606 * @return array
607 * Keys are the original keys of $records; values are the $prop values.
608 */
609 public static function collect($prop, $records) {
610 $result = array();
611 if (is_array($records)) {
612 foreach ($records as $key => $record) {
613 if (is_object($record)) {
614 $result[$key] = $record->{$prop};
615 }
616 else {
617 $result[$key] = $record[$prop];
618 }
619 }
620 }
621 return $result;
622 }
623
624 /**
625 * Iterates over a list of objects and executes some method on each.
626 *
627 * Comparison:
628 * - This is like array_map(), except it executes the objects' method
629 * instead of a free-form callable.
630 * - This is like Array::collect(), except it uses a method
631 * instead of a property.
632 *
633 * @param string $method
634 * The method to execute.
635 * @param array|Traversable $objects
636 * A list of objects.
637 * @param array $args
638 * An optional list of arguments to pass to the method.
639 *
640 * @return array
641 * Keys are the original keys of $objects; values are the method results.
642 */
643 public static function collectMethod($method, $objects, $args = array()) {
644 $result = array();
645 if (is_array($objects)) {
646 foreach ($objects as $key => $object) {
647 $result[$key] = call_user_func_array(array($object, $method), $args);
648 }
649 }
650 return $result;
651 }
652
653 /**
654 * Trims delimiters from a string and then splits it using explode().
655 *
656 * This method works mostly like PHP's built-in explode(), except that
657 * surrounding delimiters are trimmed before explode() is called.
658 *
659 * Also, if an array or NULL is passed as the $values parameter, the value is
660 * returned unmodified rather than being passed to explode().
661 *
662 * @param array|null|string $values
663 * The input string (or an array, or NULL).
664 * @param string $delim
665 * (optional) The boundary string.
666 *
667 * @return array|null
668 * An array of strings produced by explode(), or the unmodified input
669 * array, or NULL.
670 */
671 public static function explodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
672 if ($values === NULL) {
673 return NULL;
674 }
675 // If we already have an array, no need to continue
676 if (is_array($values)) {
677 return $values;
678 }
679 // Empty string -> empty array
680 if ($values === '') {
681 return array();
682 }
683 return explode($delim, trim((string) $values, $delim));
684 }
685
686 /**
687 * Joins array elements with a string, adding surrounding delimiters.
688 *
689 * This method works mostly like PHP's built-in implode(), but the generated
690 * string is surrounded by delimiter characters. Also, if NULL is passed as
691 * the $values parameter, NULL is returned.
692 *
693 * @param mixed $values
694 * Array to be imploded. If a non-array is passed, it will be cast to an
695 * array.
696 * @param string $delim
697 * Delimiter to be used for implode() and which will surround the output
698 * string.
699 *
700 * @return string|NULL
701 * The generated string, or NULL if NULL was passed as $values parameter.
702 */
703 public static function implodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) {
704 if ($values === NULL) {
705 return NULL;
706 }
707 // If we already have a string, strip $delim off the ends so it doesn't get added twice
708 if (is_string($values)) {
709 $values = trim($values, $delim);
710 }
711 return $delim . implode($delim, (array) $values) . $delim;
712 }
713
714 /**
715 * Modifies a key in an array while preserving the key order.
716 *
717 * By default when an element is added to an array, it is added to the end.
718 * This method allows for changing an existing key while preserving its
719 * position in the array.
720 *
721 * The array is both modified in-place and returned.
722 *
723 * @param array $elementArray
724 * Array to manipulate.
725 * @param string $oldKey
726 * Old key to be replaced.
727 * @param string $newKey
728 * Replacement key string.
729 *
730 * @throws Exception
731 * Throws a generic Exception if $oldKey is not found in $elementArray.
732 *
733 * @return array
734 * The manipulated array.
735 */
736 public static function crmReplaceKey(&$elementArray, $oldKey, $newKey) {
737 $keys = array_keys($elementArray);
738 if (FALSE === $index = array_search($oldKey, $keys)) {
739 throw new Exception(sprintf('key "%s" does not exit', $oldKey));
740 }
741 $keys[$index] = $newKey;
742 $elementArray = array_combine($keys, array_values($elementArray));
743 return $elementArray;
744 }
745
746 /**
747 * Searches array keys by regex, returning the value of the first match.
748 *
749 * Given a regular expression and an array, this method searches the keys
750 * of the array using the regular expression. The first match is then used
751 * to index into the array, and the associated value is retrieved and
752 * returned. If no matches are found, or if something other than an array
753 * is passed, then a default value is returned. Unless otherwise specified,
754 * the default value is NULL.
755 *
756 * @param string $regexKey
757 * The regular expression to use when searching for matching keys.
758 * @param array $list
759 * The array whose keys will be searched.
760 * @param mixed $default
761 * (optional) The default value to return if the regex does not match an
762 * array key, or if something other than an array is passed.
763 *
764 * @return mixed
765 * The value found.
766 */
767 public static function valueByRegexKey($regexKey, $list, $default = NULL) {
768 if (is_array($list) && $regexKey) {
769 $matches = preg_grep($regexKey, array_keys($list));
770 $key = reset($matches);
771 return ($key && array_key_exists($key, $list)) ? $list[$key] : $default;
772 }
773 return $default;
774 }
775
776 /**
777 * Generates the Cartesian product of zero or more vectors.
778 *
779 * @param array $dimensions
780 * List of dimensions to multiply.
781 * Each key is a dimension name; each value is a vector.
782 * @param array $template
783 * (optional) A base set of values included in every output.
784 *
785 * @return array
786 * Each item is a distinct combination of values from $dimensions.
787 *
788 * For example, the product of
789 * {
790 * fg => {red, blue},
791 * bg => {white, black}
792 * }
793 * would be
794 * {
795 * {fg => red, bg => white},
796 * {fg => red, bg => black},
797 * {fg => blue, bg => white},
798 * {fg => blue, bg => black}
799 * }
800 */
801 public static function product($dimensions, $template = array()) {
802 if (empty($dimensions)) {
803 return array($template);
804 }
805
806 foreach ($dimensions as $key => $value) {
807 $firstKey = $key;
808 $firstValues = $value;
809 break;
810 }
811 unset($dimensions[$key]);
812
813 $results = array();
814 foreach ($firstValues as $firstValue) {
815 foreach (self::product($dimensions, $template) as $result) {
816 $result[$firstKey] = $firstValue;
817 $results[] = $result;
818 }
819 }
820
821 return $results;
822 }
823
824 /**
825 * Get the first element of an array.
826 *
827 * @param array $array
828 * @return mixed|NULL
829 */
830 public static function first($array) {
831 foreach ($array as $value) {
832 return $value;
833 }
834 return NULL;
835 }
836
837 /**
838 * Extract any $keys from $array and copy to a new array.
839 *
840 * Note: If a $key does not appear in $array, then it will
841 * not appear in the result.
842 *
843 * @param array $array
844 * @param array $keys
845 * List of keys to copy.
846 * @return array
847 */
848 public static function subset($array, $keys) {
849 $result = array();
850 foreach ($keys as $key) {
851 if (isset($array[$key])) {
852 $result[$key] = $array[$key];
853 }
854 }
855 return $result;
856 }
857
858 /**
859 * Transform an associative array of key=>value pairs into a non-associative array of arrays.
860 * This is necessary to preserve sort order when sending an array through json_encode.
861 *
862 * @param array $associative
863 * @param string $keyName
864 * @param string $valueName
865 * @return array
866 */
867 public static function makeNonAssociative($associative, $keyName = 'key', $valueName = 'value') {
868 $output = array();
869 foreach ($associative as $key => $val) {
870 $output[] = array($keyName => $key, $valueName => $val);
871 }
872 return $output;
873 }
874
875 /**
876 * Diff multidimensional arrays
877 * (array_diff does not support multidimensional array)
878 *
879 * @param array $array1
880 * @param array $array2
881 * @return array
882 */
883 public static function multiArrayDiff($array1, $array2) {
884 $arrayDiff = array();
885 foreach ($array1 as $mKey => $mValue) {
886 if (array_key_exists($mKey, $array2)) {
887 if (is_array($mValue)) {
888 $recursiveDiff = self::multiArrayDiff($mValue, $array2[$mKey]);
889 if (count($recursiveDiff)) {
890 $arrayDiff[$mKey] = $recursiveDiff;
891 }
892 }
893 else {
894 if ($mValue != $array2[$mKey]) {
895 $arrayDiff[$mKey] = $mValue;
896 }
897 }
898 }
899 else {
900 $arrayDiff[$mKey] = $mValue;
901 }
902 }
903 return $arrayDiff;
904 }
905
906 /**
907 * Given a 2-dimensional matrix, create a new matrix with a restricted list of columns.
908 *
909 * @param array $matrix
910 * All matrix data, as a list of rows.
911 * @param array $columns
912 * List of column names.
913 * @return array
914 */
915 public static function filterColumns($matrix, $columns) {
916 $newRows = array();
917 foreach ($matrix as $pos => $oldRow) {
918 $newRow = array();
919 foreach ($columns as $column) {
920 $newRow[$column] = CRM_Utils_Array::value($column, $oldRow);
921 }
922 $newRows[$pos] = $newRow;
923 }
924 return $newRows;
925 }
926
927 /**
928 * Rewrite the keys in an array.
929 *
930 * @param array $array
931 * @param string|callable $indexBy
932 * Either the value to key by, or a function($key, $value) that returns the new key.
933 * @return array
934 */
935 public static function rekey($array, $indexBy) {
936 $result = array();
937 foreach ($array as $key => $value) {
938 $newKey = is_callable($indexBy) ? $indexBy($key, $value) : $value[$indexBy];
939 $result[$newKey] = $value;
940 }
941 return $result;
942 }
943
944 /**
945 * Copy all properties of $other into $array (recursively).
946 *
947 * @param array|ArrayAccess $array
948 * @param array $other
949 */
950 public static function extend(&$array, $other) {
951 foreach ($other as $key => $value) {
952 if (is_array($value)) {
953 self::extend($array[$key], $value);
954 }
955 else {
956 $array[$key] = $value;
957 }
958 }
959 }
960
961 /**
962 * Get a single value from an array-tre.
963 *
964 * @param array $arr
965 * Ex: array('foo'=>array('bar'=>123)).
966 * @param array $pathParts
967 * Ex: array('foo',bar').
968 * @return mixed|NULL
969 * Ex 123.
970 */
971 public static function pathGet($arr, $pathParts) {
972 $r = $arr;
973 foreach ($pathParts as $part) {
974 if (!isset($r[$part])) {
975 return NULL;
976 }
977 $r = $r[$part];
978 }
979 return $r;
980 }
981
982 /**
983 * Set a single value in an array tree.
984 *
985 * @param array $arr
986 * Ex: array('foo'=>array('bar'=>123)).
987 * @param array $pathParts
988 * Ex: array('foo',bar').
989 * @param $value
990 * Ex: 456.
991 */
992 public static function pathSet(&$arr, $pathParts, $value) {
993 $r = &$arr;
994 $last = array_pop($pathParts);
995 foreach ($pathParts as $part) {
996 if (!isset($r[$part])) {
997 $r[$part] = array();
998 }
999 $r = &$r[$part];
1000 }
1001 $r[$last] = $value;
1002 }
1003
1004 /**
1005 * Convert a simple dictionary into separate key+value records.
1006 *
1007 * @param array $array
1008 * Ex: array('foo' => 'bar').
1009 * @param string $keyField
1010 * Ex: 'key'.
1011 * @param string $valueField
1012 * Ex: 'value'.
1013 * @return array
1014 * Ex: array(
1015 * 0 => array('key' => 'foo', 'value' => 'bar')
1016 * ).
1017 */
1018 public static function toKeyValueRows($array, $keyField = 'key', $valueField = 'value') {
1019 $result = array();
1020 foreach ($array as $key => $value) {
1021 $result[] = array(
1022 $keyField => $key,
1023 $valueField => $value,
1024 );
1025 }
1026 return $result;
1027 }
1028
1029 /**
1030 * Convert array where key(s) holds the actual value and value(s) as 1 into array of actual values
1031 * Ex: array('foobar' => 1, 4 => 1) formatted into array('foobar', 4)
1032 *
1033 * @deprecated use convertCheckboxInputToArray instead (after testing)
1034 * https://github.com/civicrm/civicrm-core/pull/8169
1035 *
1036 * @param array $array
1037 */
1038 public static function formatArrayKeys(&$array) {
1039 if (!is_array($array)) {
1040 return;
1041 }
1042 $keys = array_keys($array, 1);
1043 if (count($keys) > 1 ||
1044 (count($keys) == 1 &&
1045 (current($keys) > 1 ||
1046 is_string(current($keys)) ||
1047 (current($keys) == 1 && $array[1] == 1) // handle (0 => 4), (1 => 1)
1048 )
1049 )
1050 ) {
1051 $array = $keys;
1052 }
1053 }
1054
1055 /**
1056 * Convert the data format coming in from checkboxes to an array of values.
1057 *
1058 * The input format from check boxes looks like
1059 * array('value1' => 1, 'value2' => 1). This function converts those values to
1060 * array(''value1', 'value2).
1061 *
1062 * The function will only alter the array if all values are equal to 1.
1063 *
1064 * @param array $input
1065 *
1066 * @return array
1067 */
1068 public static function convertCheckboxFormatToArray($input) {
1069 if (isset($input[0])) {
1070 return $input;
1071 }
1072 $keys = array_keys($input, 1);
1073 if ((count($keys) == count($input))) {
1074 return $keys;
1075 }
1076 return $input;
1077 }
1078
1079 /**
1080 * Ensure that array is encoded in utf8 format.
1081 *
1082 * @param array $array
1083 *
1084 * @return array $array utf8-encoded.
1085 */
1086 public static function encode_items($array) {
1087 foreach ($array as $key => $value) {
1088 if (is_array($value)) {
1089 $array[$key] = self::encode_items($value);
1090 }
1091 elseif (is_string($value)) {
1092 $array[$key] = mb_convert_encoding($value, mb_detect_encoding($value, mb_detect_order(), TRUE), 'UTF-8');
1093 }
1094 else {
1095 $array[$key] = $value;
1096 }
1097 }
1098 return $array;
1099 }
1100
1101 /**
1102 * Build tree of elements.
1103 *
1104 * @param array $elements
1105 * @param int|null $parentId
1106 *
1107 * @return array
1108 */
1109 public static function buildTree($elements, $parentId = NULL) {
1110 $branch = array();
1111
1112 foreach ($elements as $element) {
1113 if ($element['parent_id'] == $parentId) {
1114 $children = self::buildTree($elements, $element['id']);
1115 if ($children) {
1116 $element['children'] = $children;
1117 }
1118 $branch[] = $element;
1119 }
1120 }
1121
1122 return $branch;
1123 }
1124
1125 /**
1126 * Find search string in tree.
1127 *
1128 * @param string $search
1129 * @param array $tree
1130 * @param string $field
1131 *
1132 * @return array|null
1133 */
1134 public static function findInTree($search, $tree, $field = 'id') {
1135 foreach ($tree as $item) {
1136 if ($item[$field] == $search) {
1137 return $item;
1138 }
1139 if (!empty($item['children'])) {
1140 $found = self::findInTree($search, $item['children']);
1141 if ($found) {
1142 return $found;
1143 }
1144 }
1145 }
1146 return NULL;
1147 }
1148
1149 }