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