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