Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
7e9e8871 | 4 | | CiviCRM version 4.7 | |
6a488035 | 5 | +--------------------------------------------------------------------+ |
fa938177 | 6 | | Copyright CiviCRM LLC (c) 2004-2016 | |
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 | |
fa938177 | 32 | * @copyright CiviCRM LLC (c) 2004-2016 |
6a488035 TO |
33 | */ |
34 | class 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) { | |
174 | $src = array('&', '<', '>', '\ 1'); | |
175 | $dst = array('&', '<', '>', ','); | |
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 { | |
241 | if (!empty($value)) { | |
242 | $flat[$newPrefix] = $value; | |
243 | } | |
244 | } | |
245 | } | |
246 | } | |
247 | ||
248 | /** | |
c9e15d2a RS |
249 | * Converts an array with path-like keys into a tree of arrays. |
250 | * | |
251 | * This function is the inverse of CRM_Utils_Array::flatten(). | |
252 | * | |
253 | * @param string $delim | |
254 | * A path delimiter | |
255 | * @param array $arr | |
256 | * A one-dimensional array indexed by string keys | |
6a488035 | 257 | * |
c9e15d2a RS |
258 | * @return array |
259 | * Array-encoded tree | |
6a488035 | 260 | */ |
00be9182 | 261 | public function unflatten($delim, &$arr) { |
6a488035 TO |
262 | $result = array(); |
263 | foreach ($arr as $key => $value) { | |
264 | $path = explode($delim, $key); | |
265 | $node = &$result; | |
266 | while (count($path) > 1) { | |
267 | $key = array_shift($path); | |
268 | if (!isset($node[$key])) { | |
269 | $node[$key] = array(); | |
270 | } | |
271 | $node = &$node[$key]; | |
272 | } | |
273 | // last part of path | |
274 | $key = array_shift($path); | |
275 | $node[$key] = $value; | |
276 | } | |
277 | return $result; | |
278 | } | |
279 | ||
280 | /** | |
c9e15d2a RS |
281 | * Merges two arrays. |
282 | * | |
283 | * If $a1[foo] and $a2[foo] both exist and are both arrays, the merge | |
284 | * process recurses into those sub-arrays. If $a1[foo] and $a2[foo] both | |
285 | * exist but they are not both arrays, the value from $a1 overrides the | |
286 | * value from $a2 and the value from $a2 is discarded. | |
6a488035 TO |
287 | * |
288 | * @param array $a1 | |
c9e15d2a | 289 | * First array to be merged. |
6a488035 | 290 | * @param array $a2 |
c9e15d2a | 291 | * Second array to be merged. |
6a488035 | 292 | * |
c9e15d2a RS |
293 | * @return array |
294 | * The merged array. | |
6a488035 | 295 | */ |
00be9182 | 296 | public static function crmArrayMerge($a1, $a2) { |
6a488035 TO |
297 | if (empty($a1)) { |
298 | return $a2; | |
299 | } | |
300 | ||
301 | if (empty($a2)) { | |
302 | return $a1; | |
303 | } | |
304 | ||
305 | $a3 = array(); | |
306 | foreach ($a1 as $key => $value) { | |
307 | if (array_key_exists($key, $a2) && | |
308 | is_array($a2[$key]) && is_array($a1[$key]) | |
309 | ) { | |
310 | $a3[$key] = array_merge($a1[$key], $a2[$key]); | |
311 | } | |
312 | else { | |
313 | $a3[$key] = $a1[$key]; | |
314 | } | |
315 | } | |
316 | ||
317 | foreach ($a2 as $key => $value) { | |
318 | if (array_key_exists($key, $a1)) { | |
319 | // already handled in above loop | |
320 | continue; | |
321 | } | |
322 | $a3[$key] = $a2[$key]; | |
323 | } | |
324 | ||
325 | return $a3; | |
326 | } | |
327 | ||
c9e15d2a RS |
328 | /** |
329 | * Determines whether an array contains any sub-arrays. | |
330 | * | |
331 | * @param array $list | |
332 | * The array to inspect. | |
333 | * | |
334 | * @return bool | |
335 | * True if $list contains at least one sub-array, false otherwise. | |
c9e15d2a | 336 | */ |
00be9182 | 337 | public static function isHierarchical(&$list) { |
6a488035 TO |
338 | foreach ($list as $n => $v) { |
339 | if (is_array($v)) { | |
340 | return TRUE; | |
341 | } | |
342 | } | |
343 | return FALSE; | |
344 | } | |
345 | ||
82376c19 | 346 | /** |
54957108 | 347 | * Is array A a subset of array B. |
348 | * | |
349 | * @param array $subset | |
350 | * @param array $superset | |
351 | * | |
a6c01b45 CW |
352 | * @return bool |
353 | * TRUE if $subset is a subset of $superset | |
82376c19 | 354 | */ |
00be9182 | 355 | public static function isSubset($subset, $superset) { |
82376c19 TO |
356 | foreach ($subset as $expected) { |
357 | if (!in_array($expected, $superset)) { | |
358 | return FALSE; | |
359 | } | |
360 | } | |
361 | return TRUE; | |
362 | } | |
363 | ||
6a488035 | 364 | /** |
c9e15d2a | 365 | * Searches an array recursively in an optionally case-insensitive manner. |
6a488035 | 366 | * |
c9e15d2a RS |
367 | * @param string $value |
368 | * Value to search for. | |
369 | * @param array $params | |
370 | * Array to search within. | |
371 | * @param bool $caseInsensitive | |
372 | * (optional) Whether to search in a case-insensitive manner. | |
6a488035 | 373 | * |
c9e15d2a RS |
374 | * @return bool |
375 | * True if $value was found, false otherwise. | |
6a488035 | 376 | */ |
00be9182 | 377 | public static function crmInArray($value, $params, $caseInsensitive = TRUE) { |
6a488035 TO |
378 | foreach ($params as $item) { |
379 | if (is_array($item)) { | |
380 | $ret = crmInArray($value, $item, $caseInsensitive); | |
381 | } | |
382 | else { | |
383 | $ret = ($caseInsensitive) ? strtolower($item) == strtolower($value) : $item == $value; | |
384 | if ($ret) { | |
385 | return $ret; | |
386 | } | |
387 | } | |
388 | } | |
389 | return FALSE; | |
390 | } | |
391 | ||
392 | /** | |
54957108 | 393 | * Convert associative array names to values and vice-versa. |
6a488035 TO |
394 | * |
395 | * This function is used by both the web form layer and the api. Note that | |
396 | * the api needs the name => value conversion, also the view layer typically | |
397 | * requires value => name conversion | |
54957108 | 398 | * |
399 | * @param array $defaults | |
400 | * @param string $property | |
401 | * @param $lookup | |
402 | * @param $reverse | |
403 | * | |
404 | * @return bool | |
6a488035 | 405 | */ |
00be9182 | 406 | public static function lookupValue(&$defaults, $property, $lookup, $reverse) { |
6a488035 TO |
407 | $id = $property . '_id'; |
408 | ||
409 | $src = $reverse ? $property : $id; | |
410 | $dst = $reverse ? $id : $property; | |
411 | ||
412 | if (!array_key_exists(strtolower($src), array_change_key_case($defaults, CASE_LOWER))) { | |
413 | return FALSE; | |
414 | } | |
415 | ||
416 | $look = $reverse ? array_flip($lookup) : $lookup; | |
417 | ||
50bfb460 | 418 | // trim lookup array, ignore . ( fix for CRM-1514 ), eg for prefix/suffix make sure Dr. and Dr both are valid |
6a488035 TO |
419 | $newLook = array(); |
420 | foreach ($look as $k => $v) { | |
421 | $newLook[trim($k, ".")] = $v; | |
422 | } | |
423 | ||
424 | $look = $newLook; | |
425 | ||
426 | if (is_array($look)) { | |
427 | if (!array_key_exists(trim(strtolower($defaults[strtolower($src)]), '.'), array_change_key_case($look, CASE_LOWER))) { | |
428 | return FALSE; | |
429 | } | |
430 | } | |
431 | ||
432 | $tempLook = array_change_key_case($look, CASE_LOWER); | |
433 | ||
434 | $defaults[$dst] = $tempLook[trim(strtolower($defaults[strtolower($src)]), '.')]; | |
435 | return TRUE; | |
436 | } | |
437 | ||
438 | /** | |
c9e15d2a RS |
439 | * Checks whether an array is empty. |
440 | * | |
441 | * An array is empty if its values consist only of NULL and empty sub-arrays. | |
442 | * Containing a non-NULL value or non-empty array makes an array non-empty. | |
443 | * | |
444 | * If something other than an array is passed, it is considered to be empty. | |
445 | * | |
446 | * If nothing is passed at all, the default value provided is empty. | |
447 | * | |
448 | * @param array $array | |
449 | * (optional) Array to be checked for emptiness. | |
6a488035 | 450 | * |
d5cc0fc2 | 451 | * @return bool |
c9e15d2a | 452 | * True if the array is empty. |
6a488035 | 453 | */ |
00be9182 | 454 | public static function crmIsEmptyArray($array = array()) { |
6a488035 TO |
455 | if (!is_array($array)) { |
456 | return TRUE; | |
457 | } | |
458 | foreach ($array as $element) { | |
459 | if (is_array($element)) { | |
460 | if (!self::crmIsEmptyArray($element)) { | |
461 | return FALSE; | |
462 | } | |
463 | } | |
464 | elseif (isset($element)) { | |
465 | return FALSE; | |
466 | } | |
467 | } | |
468 | return TRUE; | |
469 | } | |
470 | ||
6a488035 | 471 | /** |
c9e15d2a | 472 | * Sorts an associative array of arrays by an attribute using strnatcmp(). |
6a488035 | 473 | * |
c9e15d2a RS |
474 | * @param array $array |
475 | * Array to be sorted. | |
476 | * @param string $field | |
477 | * Name of the attribute used for sorting. | |
6a488035 | 478 | * |
f4aaa82a | 479 | * @return array |
c9e15d2a | 480 | * Sorted array |
6a488035 | 481 | */ |
00be9182 | 482 | public static function crmArraySortByField($array, $field) { |
6a488035 TO |
483 | $code = "return strnatcmp(\$a['$field'], \$b['$field']);"; |
484 | uasort($array, create_function('$a,$b', $code)); | |
485 | return $array; | |
486 | } | |
487 | ||
488 | /** | |
c9e15d2a | 489 | * Recursively removes duplicate values from a multi-dimensional array. |
6a488035 | 490 | * |
c9e15d2a RS |
491 | * @param array $array |
492 | * The input array possibly containing duplicate values. | |
6a488035 | 493 | * |
f4aaa82a | 494 | * @return array |
c9e15d2a | 495 | * The input array with duplicate values removed. |
6a488035 | 496 | */ |
00be9182 | 497 | public static function crmArrayUnique($array) { |
6a488035 TO |
498 | $result = array_map("unserialize", array_unique(array_map("serialize", $array))); |
499 | foreach ($result as $key => $value) { | |
500 | if (is_array($value)) { | |
501 | $result[$key] = self::crmArrayUnique($value); | |
502 | } | |
503 | } | |
504 | return $result; | |
505 | } | |
506 | ||
507 | /** | |
c9e15d2a RS |
508 | * Sorts an array and maintains index association (with localization). |
509 | * | |
510 | * Uses Collate from the PECL "intl" package, if available, for UTF-8 | |
511 | * sorting (e.g. list of countries). Otherwise calls PHP's asort(). | |
6a488035 | 512 | * |
c9e15d2a | 513 | * On Debian/Ubuntu: apt-get install php5-intl |
6a488035 | 514 | * |
c9e15d2a RS |
515 | * @param array $array |
516 | * (optional) Array to be sorted. | |
517 | * | |
f4aaa82a | 518 | * @return array |
c9e15d2a | 519 | * Sorted array. |
6a488035 | 520 | */ |
00be9182 | 521 | public static function asort($array = array()) { |
6a488035 TO |
522 | $lcMessages = CRM_Utils_System::getUFLocale(); |
523 | ||
524 | if ($lcMessages && $lcMessages != 'en_US' && class_exists('Collator')) { | |
525 | $collator = new Collator($lcMessages . '.utf8'); | |
526 | $collator->asort($array); | |
527 | } | |
528 | else { | |
c9e15d2a | 529 | // This calls PHP's built-in asort(). |
6a488035 TO |
530 | asort($array); |
531 | } | |
532 | ||
533 | return $array; | |
534 | } | |
535 | ||
536 | /** | |
c9e15d2a RS |
537 | * Unsets an arbitrary list of array elements from an associative array. |
538 | * | |
539 | * @param array $items | |
540 | * The array from which to remove items. | |
f4aaa82a | 541 | * |
fa0ededf CW |
542 | * Additional params: |
543 | * When passed a string, unsets $items[$key]. | |
544 | * When passed an array of strings, unsets $items[$k] for each string $k in the array. | |
6a488035 | 545 | */ |
e7292422 TO |
546 | public static function remove(&$items) { |
547 | foreach (func_get_args() as $n => $key) { | |
548 | // Skip argument 0 ($items) by testing $n for truth. | |
549 | if ($n && is_array($key)) { | |
22e263ad | 550 | foreach ($key as $k) { |
e7292422 TO |
551 | unset($items[$k]); |
552 | } | |
553 | } | |
554 | elseif ($n) { | |
555 | unset($items[$key]); | |
556 | } | |
557 | } | |
558 | } | |
6a488035 TO |
559 | |
560 | /** | |
c9e15d2a | 561 | * Builds an array-tree which indexes the records in an array. |
6a488035 | 562 | * |
c9e15d2a RS |
563 | * @param string[] $keys |
564 | * Properties by which to index. | |
565 | * @param object|array $records | |
566 | * | |
567 | * @return array | |
568 | * Multi-dimensional array, with one layer for each key. | |
6a488035 | 569 | */ |
00be9182 | 570 | public static function index($keys, $records) { |
6a488035 TO |
571 | $final_key = array_pop($keys); |
572 | ||
573 | $result = array(); | |
574 | foreach ($records as $record) { | |
575 | $node = &$result; | |
576 | foreach ($keys as $key) { | |
577 | if (is_array($record)) { | |
928c5201 | 578 | $keyvalue = isset($record[$key]) ? $record[$key] : NULL; |
0db6c3e1 TO |
579 | } |
580 | else { | |
928c5201 | 581 | $keyvalue = isset($record->{$key}) ? $record->{$key} : NULL; |
6a488035 | 582 | } |
17d4d611 | 583 | if (isset($node[$keyvalue]) && !is_array($node[$keyvalue])) { |
6a488035 TO |
584 | $node[$keyvalue] = array(); |
585 | } | |
586 | $node = &$node[$keyvalue]; | |
587 | } | |
588 | if (is_array($record)) { | |
e7292422 | 589 | $node[$record[$final_key]] = $record; |
0db6c3e1 TO |
590 | } |
591 | else { | |
e7292422 | 592 | $node[$record->{$final_key}] = $record; |
6a488035 TO |
593 | } |
594 | } | |
595 | return $result; | |
596 | } | |
597 | ||
598 | /** | |
c9e15d2a | 599 | * Iterates over a list of records and returns the value of some property. |
6a488035 TO |
600 | * |
601 | * @param string $prop | |
c9e15d2a RS |
602 | * Property to retrieve. |
603 | * @param array|object $records | |
604 | * A list of records. | |
605 | * | |
606 | * @return array | |
607 | * Keys are the original keys of $records; values are the $prop values. | |
6a488035 | 608 | */ |
00be9182 | 609 | public static function collect($prop, $records) { |
6a488035 | 610 | $result = array(); |
4d34fcfa | 611 | if (is_array($records)) { |
612 | foreach ($records as $key => $record) { | |
613 | if (is_object($record)) { | |
614 | $result[$key] = $record->{$prop}; | |
0db6c3e1 TO |
615 | } |
616 | else { | |
4d34fcfa | 617 | $result[$key] = $record[$prop]; |
618 | } | |
6a488035 TO |
619 | } |
620 | } | |
621 | return $result; | |
622 | } | |
623 | ||
2c6fe88a TO |
624 | /** |
625 | * Iterates over a list of objects and executes some method on each. | |
626 | * | |
627 | * Comparison: | |
628 | * - This is like array_map(), except it executes the objects' method | |
629 | * instead of a free-form callable. | |
630 | * - This is like Array::collect(), except it uses a method | |
631 | * instead of a property. | |
632 | * | |
633 | * @param string $method | |
634 | * The method to execute. | |
635 | * @param array|Traversable $objects | |
636 | * A list of objects. | |
637 | * @param array $args | |
638 | * An optional list of arguments to pass to the method. | |
639 | * | |
640 | * @return array | |
641 | * Keys are the original keys of $objects; values are the method results. | |
642 | */ | |
643 | public static function collectMethod($method, $objects, $args = array()) { | |
644 | $result = array(); | |
645 | if (is_array($objects)) { | |
646 | foreach ($objects as $key => $object) { | |
647 | $result[$key] = call_user_func_array(array($object, $method), $args); | |
648 | } | |
649 | } | |
650 | return $result; | |
651 | } | |
652 | ||
6a488035 | 653 | /** |
c9e15d2a | 654 | * Trims delimiters from a string and then splits it using explode(). |
6a488035 | 655 | * |
c9e15d2a RS |
656 | * This method works mostly like PHP's built-in explode(), except that |
657 | * surrounding delimiters are trimmed before explode() is called. | |
658 | * | |
659 | * Also, if an array or NULL is passed as the $values parameter, the value is | |
660 | * returned unmodified rather than being passed to explode(). | |
661 | * | |
662 | * @param array|null|string $values | |
663 | * The input string (or an array, or NULL). | |
6a488035 | 664 | * @param string $delim |
c9e15d2a RS |
665 | * (optional) The boundary string. |
666 | * | |
667 | * @return array|null | |
668 | * An array of strings produced by explode(), or the unmodified input | |
669 | * array, or NULL. | |
6a488035 | 670 | */ |
00be9182 | 671 | public static function explodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) { |
fe18a93c | 672 | if ($values === NULL) { |
6a488035 TO |
673 | return NULL; |
674 | } | |
fe18a93c CW |
675 | // If we already have an array, no need to continue |
676 | if (is_array($values)) { | |
677 | return $values; | |
678 | } | |
62bcdea6 CW |
679 | // Empty string -> empty array |
680 | if ($values === '') { | |
681 | return array(); | |
682 | } | |
fe18a93c | 683 | return explode($delim, trim((string) $values, $delim)); |
6a488035 TO |
684 | } |
685 | ||
686 | /** | |
c9e15d2a RS |
687 | * Joins array elements with a string, adding surrounding delimiters. |
688 | * | |
689 | * This method works mostly like PHP's built-in implode(), but the generated | |
690 | * string is surrounded by delimiter characters. Also, if NULL is passed as | |
691 | * the $values parameter, NULL is returned. | |
6a488035 | 692 | * |
fe18a93c | 693 | * @param mixed $values |
c9e15d2a RS |
694 | * Array to be imploded. If a non-array is passed, it will be cast to an |
695 | * array. | |
6a488035 | 696 | * @param string $delim |
c9e15d2a RS |
697 | * Delimiter to be used for implode() and which will surround the output |
698 | * string. | |
699 | * | |
fe18a93c | 700 | * @return string|NULL |
c9e15d2a | 701 | * The generated string, or NULL if NULL was passed as $values parameter. |
6a488035 | 702 | */ |
00be9182 | 703 | public static function implodePadded($values, $delim = CRM_Core_DAO::VALUE_SEPARATOR) { |
6a488035 TO |
704 | if ($values === NULL) { |
705 | return NULL; | |
706 | } | |
fe18a93c CW |
707 | // If we already have a string, strip $delim off the ends so it doesn't get added twice |
708 | if (is_string($values)) { | |
709 | $values = trim($values, $delim); | |
710 | } | |
711 | return $delim . implode($delim, (array) $values) . $delim; | |
6a488035 TO |
712 | } |
713 | ||
714 | /** | |
c9e15d2a RS |
715 | * Modifies a key in an array while preserving the key order. |
716 | * | |
717 | * By default when an element is added to an array, it is added to the end. | |
718 | * This method allows for changing an existing key while preserving its | |
719 | * position in the array. | |
720 | * | |
721 | * The array is both modified in-place and returned. | |
722 | * | |
723 | * @param array $elementArray | |
724 | * Array to manipulate. | |
725 | * @param string $oldKey | |
726 | * Old key to be replaced. | |
727 | * @param string $newKey | |
728 | * Replacement key string. | |
6a488035 | 729 | * |
c9e15d2a RS |
730 | * @throws Exception |
731 | * Throws a generic Exception if $oldKey is not found in $elementArray. | |
6a488035 TO |
732 | * |
733 | * @return array | |
c9e15d2a | 734 | * The manipulated array. |
6a488035 | 735 | */ |
00be9182 | 736 | public static function crmReplaceKey(&$elementArray, $oldKey, $newKey) { |
6a488035 TO |
737 | $keys = array_keys($elementArray); |
738 | if (FALSE === $index = array_search($oldKey, $keys)) { | |
739 | throw new Exception(sprintf('key "%s" does not exit', $oldKey)); | |
740 | } | |
741 | $keys[$index] = $newKey; | |
742 | $elementArray = array_combine($keys, array_values($elementArray)); | |
743 | return $elementArray; | |
744 | } | |
bd7b39e7 | 745 | |
d424ffde | 746 | /** |
c9e15d2a RS |
747 | * Searches array keys by regex, returning the value of the first match. |
748 | * | |
749 | * Given a regular expression and an array, this method searches the keys | |
750 | * of the array using the regular expression. The first match is then used | |
751 | * to index into the array, and the associated value is retrieved and | |
752 | * returned. If no matches are found, or if something other than an array | |
753 | * is passed, then a default value is returned. Unless otherwise specified, | |
754 | * the default value is NULL. | |
755 | * | |
756 | * @param string $regexKey | |
757 | * The regular expression to use when searching for matching keys. | |
758 | * @param array $list | |
759 | * The array whose keys will be searched. | |
760 | * @param mixed $default | |
761 | * (optional) The default value to return if the regex does not match an | |
762 | * array key, or if something other than an array is passed. | |
763 | * | |
764 | * @return mixed | |
765 | * The value found. | |
bd7b39e7 | 766 | */ |
00be9182 | 767 | public static function valueByRegexKey($regexKey, $list, $default = NULL) { |
bd7b39e7 PJ |
768 | if (is_array($list) && $regexKey) { |
769 | $matches = preg_grep($regexKey, array_keys($list)); | |
770 | $key = reset($matches); | |
771 | return ($key && array_key_exists($key, $list)) ? $list[$key] : $default; | |
772 | } | |
773 | return $default; | |
774 | } | |
17deafad TO |
775 | |
776 | /** | |
c9e15d2a | 777 | * Generates the Cartesian product of zero or more vectors. |
17deafad | 778 | * |
c9e15d2a RS |
779 | * @param array $dimensions |
780 | * List of dimensions to multiply. | |
781 | * Each key is a dimension name; each value is a vector. | |
782 | * @param array $template | |
783 | * (optional) A base set of values included in every output. | |
784 | * | |
785 | * @return array | |
786 | * Each item is a distinct combination of values from $dimensions. | |
17deafad | 787 | * |
d5cc0fc2 | 788 | * For example, the product of |
789 | * { | |
17deafad TO |
790 | * fg => {red, blue}, |
791 | * bg => {white, black} | |
d5cc0fc2 | 792 | * } |
793 | * would be | |
794 | * { | |
17deafad TO |
795 | * {fg => red, bg => white}, |
796 | * {fg => red, bg => black}, | |
797 | * {fg => blue, bg => white}, | |
798 | * {fg => blue, bg => black} | |
d5cc0fc2 | 799 | * } |
17deafad | 800 | */ |
00be9182 | 801 | public static function product($dimensions, $template = array()) { |
17deafad TO |
802 | if (empty($dimensions)) { |
803 | return array($template); | |
804 | } | |
805 | ||
806 | foreach ($dimensions as $key => $value) { | |
807 | $firstKey = $key; | |
808 | $firstValues = $value; | |
809 | break; | |
810 | } | |
811 | unset($dimensions[$key]); | |
812 | ||
813 | $results = array(); | |
814 | foreach ($firstValues as $firstValue) { | |
815 | foreach (self::product($dimensions, $template) as $result) { | |
816 | $result[$firstKey] = $firstValue; | |
817 | $results[] = $result; | |
818 | } | |
819 | } | |
820 | ||
821 | return $results; | |
822 | } | |
eb20fbf0 TO |
823 | |
824 | /** | |
fe482240 | 825 | * Get the first element of an array. |
eb20fbf0 TO |
826 | * |
827 | * @param array $array | |
828 | * @return mixed|NULL | |
829 | */ | |
00be9182 | 830 | public static function first($array) { |
eb20fbf0 TO |
831 | foreach ($array as $value) { |
832 | return $value; | |
833 | } | |
834 | return NULL; | |
835 | } | |
768c558c TO |
836 | |
837 | /** | |
838 | * Extract any $keys from $array and copy to a new array. | |
839 | * | |
840 | * Note: If a $key does not appear in $array, then it will | |
841 | * not appear in the result. | |
842 | * | |
843 | * @param array $array | |
77855840 TO |
844 | * @param array $keys |
845 | * List of keys to copy. | |
768c558c TO |
846 | * @return array |
847 | */ | |
00be9182 | 848 | public static function subset($array, $keys) { |
768c558c TO |
849 | $result = array(); |
850 | foreach ($keys as $key) { | |
851 | if (isset($array[$key])) { | |
852 | $result[$key] = $array[$key]; | |
853 | } | |
854 | } | |
855 | return $result; | |
856 | } | |
a335f6b2 | 857 | |
b7ceb253 CW |
858 | /** |
859 | * Transform an associative array of key=>value pairs into a non-associative array of arrays. | |
860 | * This is necessary to preserve sort order when sending an array through json_encode. | |
861 | * | |
862 | * @param array $associative | |
863 | * @param string $keyName | |
864 | * @param string $valueName | |
865 | * @return array | |
866 | */ | |
00be9182 | 867 | public static function makeNonAssociative($associative, $keyName = 'key', $valueName = 'value') { |
b7ceb253 CW |
868 | $output = array(); |
869 | foreach ($associative as $key => $val) { | |
870 | $output[] = array($keyName => $key, $valueName => $val); | |
871 | } | |
872 | return $output; | |
873 | } | |
96025800 | 874 | |
06d253f5 | 875 | /** |
876 | * Diff multidimensional arrays | |
50bfb460 | 877 | * (array_diff does not support multidimensional array) |
06d253f5 | 878 | * |
879 | * @param array $array1 | |
880 | * @param array $array2 | |
881 | * @return array | |
882 | */ | |
883 | public static function multiArrayDiff($array1, $array2) { | |
884 | $arrayDiff = array(); | |
885 | foreach ($array1 as $mKey => $mValue) { | |
886 | if (array_key_exists($mKey, $array2)) { | |
887 | if (is_array($mValue)) { | |
888 | $recursiveDiff = self::multiArrayDiff($mValue, $array2[$mKey]); | |
889 | if (count($recursiveDiff)) { | |
890 | $arrayDiff[$mKey] = $recursiveDiff; | |
891 | } | |
892 | } | |
893 | else { | |
894 | if ($mValue != $array2[$mKey]) { | |
895 | $arrayDiff[$mKey] = $mValue; | |
896 | } | |
897 | } | |
898 | } | |
899 | else { | |
900 | $arrayDiff[$mKey] = $mValue; | |
901 | } | |
902 | } | |
903 | return $arrayDiff; | |
904 | } | |
5793f1d9 | 905 | |
5bc7c754 TO |
906 | /** |
907 | * Given a 2-dimensional matrix, create a new matrix with a restricted list of columns. | |
908 | * | |
909 | * @param array $matrix | |
910 | * All matrix data, as a list of rows. | |
911 | * @param array $columns | |
912 | * List of column names. | |
913 | * @return array | |
914 | */ | |
915 | public static function filterColumns($matrix, $columns) { | |
916 | $newRows = array(); | |
917 | foreach ($matrix as $pos => $oldRow) { | |
918 | $newRow = array(); | |
919 | foreach ($columns as $column) { | |
920 | $newRow[$column] = CRM_Utils_Array::value($column, $oldRow); | |
921 | } | |
922 | $newRows[$pos] = $newRow; | |
923 | } | |
924 | return $newRows; | |
925 | } | |
926 | ||
952d7eb1 TO |
927 | /** |
928 | * Rewrite the keys in an array by filtering through a function. | |
929 | * | |
930 | * @param array $array | |
931 | * @param callable $func | |
932 | * Function($key, $value). Returns the new key. | |
933 | * @return array | |
934 | */ | |
935 | public static function rekey($array, $func) { | |
936 | $result = array(); | |
937 | foreach ($array as $key => $value) { | |
938 | $newKey = $func($key, $value); | |
939 | $result[$newKey] = $value; | |
940 | } | |
941 | return $result; | |
942 | } | |
943 | ||
4f24efcc TO |
944 | /** |
945 | * Copy all properties of $other into $array (recursively). | |
946 | * | |
947 | * @param array|ArrayAccess $array | |
948 | * @param array $other | |
949 | */ | |
950 | public static function extend(&$array, $other) { | |
951 | foreach ($other as $key => $value) { | |
952 | if (is_array($value)) { | |
953 | self::extend($array[$key], $value); | |
954 | } | |
955 | else { | |
956 | $array[$key] = $value; | |
957 | } | |
958 | } | |
959 | } | |
960 | ||
393f41dd TO |
961 | /** |
962 | * Get a single value from an array-tre. | |
963 | * | |
964 | * @param array $arr | |
965 | * Ex: array('foo'=>array('bar'=>123)). | |
966 | * @param array $pathParts | |
967 | * Ex: array('foo',bar'). | |
968 | * @return mixed|NULL | |
969 | * Ex 123. | |
970 | */ | |
971 | public static function pathGet($arr, $pathParts) { | |
972 | $r = $arr; | |
973 | foreach ($pathParts as $part) { | |
974 | if (!isset($r[$part])) { | |
975 | return NULL; | |
976 | } | |
977 | $r = $r[$part]; | |
978 | } | |
979 | return $r; | |
980 | } | |
981 | ||
982 | /** | |
983 | * Set a single value in an array tree. | |
984 | * | |
985 | * @param array $arr | |
986 | * Ex: array('foo'=>array('bar'=>123)). | |
987 | * @param array $pathParts | |
988 | * Ex: array('foo',bar'). | |
989 | * @param $value | |
990 | * Ex: 456. | |
991 | */ | |
992 | public static function pathSet(&$arr, $pathParts, $value) { | |
993 | $r = &$arr; | |
994 | $last = array_pop($pathParts); | |
995 | foreach ($pathParts as $part) { | |
996 | if (!isset($r[$part])) { | |
997 | $r[$part] = array(); | |
998 | } | |
999 | $r = &$r[$part]; | |
1000 | } | |
1001 | $r[$last] = $value; | |
1002 | } | |
1003 | ||
2c6fe88a TO |
1004 | /** |
1005 | * Convert a simple dictionary into separate key+value records. | |
1006 | * | |
1007 | * @param array $array | |
1008 | * Ex: array('foo' => 'bar'). | |
1009 | * @param string $keyField | |
1010 | * Ex: 'key'. | |
1011 | * @param string $valueField | |
1012 | * Ex: 'value'. | |
1013 | * @return array | |
1014 | * Ex: array( | |
1015 | * 0 => array('key' => 'foo', 'value' => 'bar') | |
1016 | * ). | |
1017 | */ | |
1018 | public static function toKeyValueRows($array, $keyField = 'key', $valueField = 'value') { | |
1019 | $result = array(); | |
1020 | foreach ($array as $key => $value) { | |
1021 | $result[] = array( | |
1022 | $keyField => $key, | |
1023 | $valueField => $value, | |
1024 | ); | |
1025 | } | |
1026 | return $result; | |
1027 | } | |
1028 | ||
6c28d4be | 1029 | /** |
1030 | * Convert array where key(s) holds the actual value and value(s) as 1 into array of actual values | |
1031 | * Ex: array('foobar' => 1, 4 => 1) formatted into array('foobar', 4) | |
1032 | * | |
1033 | * @param array $array | |
1034 | * @return void | |
1035 | */ | |
1036 | public static function formatArrayKeys(&$array) { | |
38c304a5 | 1037 | if (!is_array($array)) { |
1038 | return; | |
1039 | } | |
6c28d4be | 1040 | $keys = array_keys($array, 1); |
1041 | if (count($keys) > 1 || | |
1042 | (count($keys) == 1 && | |
f6cc7d47 | 1043 | (current($keys) > 1 || |
1044 | is_string(current($keys)) || | |
1045 | (current($keys) == 1 && $array[1] == 1) // handle (0 => 4), (1 => 1) | |
6c28d4be | 1046 | ) |
1047 | ) | |
1048 | ) { | |
1049 | $array = $keys; | |
1050 | } | |
1051 | } | |
1052 | ||
6a488035 | 1053 | } |