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