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