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