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