Revert "[TASK] Introduce DeprecationUtility and move methods"
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / ArrayUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Class with helper functions for array handling
19 */
20 class ArrayUtility
21 {
22 /**
23 * Reduce an array by a search value and keep the array structure.
24 *
25 * Comparison is type strict:
26 * - For a given needle of type string, integer, array or boolean,
27 * value and value type must match to occur in result array
28 * - For a given object, an object within the array must be a reference to
29 * the same object to match (not just different instance of same class)
30 *
31 * Example:
32 * - Needle: 'findMe'
33 * - Given array:
34 * array(
35 * 'foo' => 'noMatch',
36 * 'bar' => 'findMe',
37 * 'foobar => array(
38 * 'foo' => 'findMe',
39 * ),
40 * );
41 * - Result:
42 * array(
43 * 'bar' => 'findMe',
44 * 'foobar' => array(
45 * 'foo' => findMe',
46 * ),
47 * );
48 *
49 * See the unit tests for more examples and expected behaviour
50 *
51 * @param mixed $needle The value to search for
52 * @param array $haystack The array in which to search
53 * @return array $haystack array reduced matching $needle values
54 */
55 public static function filterByValueRecursive($needle = '', array $haystack = array())
56 {
57 $resultArray = array();
58 // Define a lambda function to be applied to all members of this array dimension
59 // Call recursive if current value is of type array
60 // Write to $resultArray (by reference!) if types and value match
61 $callback = function (&$value, $key) use ($needle, &$resultArray) {
62 if ($value === $needle) {
63 ($resultArray[$key] = $value);
64 } elseif (is_array($value)) {
65 ($subArrayMatches = static::filterByValueRecursive($needle, $value));
66 if (!empty($subArrayMatches)) {
67 ($resultArray[$key] = $subArrayMatches);
68 }
69 }
70 };
71 // array_walk() is not affected by the internal pointers, no need to reset
72 array_walk($haystack, $callback);
73 // Pointers to result array are reset internally
74 return $resultArray;
75 }
76
77 /**
78 * Checks if a given path exists in array
79 *
80 * Example:
81 * - array:
82 * array(
83 * 'foo' => array(
84 * 'bar' = 'test',
85 * )
86 * );
87 * - path: 'foo/bar'
88 * - return: TRUE
89 *
90 * @param array $array Given array
91 * @param string $path Path to test, 'foo/bar/foobar'
92 * @param string $delimiter Delimiter for path, default /
93 * @return bool TRUE if path exists in array
94 */
95 public static function isValidPath(array $array, $path, $delimiter = '/')
96 {
97 $isValid = true;
98 try {
99 // Use late static binding to enable mocking of this call in unit tests
100 static::getValueByPath($array, $path, $delimiter);
101 } catch (\RuntimeException $e) {
102 $isValid = false;
103 }
104 return $isValid;
105 }
106
107 /**
108 * Returns a value by given path
109 *
110 * Example
111 * - array:
112 * array(
113 * 'foo' => array(
114 * 'bar' => array(
115 * 'baz' => 42
116 * )
117 * )
118 * );
119 * - path: foo/bar/baz
120 * - return: 42
121 *
122 * If a path segments contains a delimiter character, the path segment
123 * must be enclosed by " (double quote), see unit tests for details
124 *
125 * @param array $array Input array
126 * @param string $path Path within the array
127 * @param string $delimiter Defined path delimiter, default /
128 * @return mixed
129 * @throws \RuntimeException
130 */
131 public static function getValueByPath(array $array, $path, $delimiter = '/')
132 {
133 if (empty($path)) {
134 throw new \RuntimeException('Path must not be empty', 1341397767);
135 }
136 // Extract parts of the path
137 $path = str_getcsv($path, $delimiter);
138 // Loop through each part and extract its value
139 $value = $array;
140 foreach ($path as $segment) {
141 if (array_key_exists($segment, $value)) {
142 // Replace current value with child
143 $value = $value[$segment];
144 } else {
145 // Fail if key does not exist
146 throw new \RuntimeException('Path does not exist in array', 1341397869);
147 }
148 }
149 return $value;
150 }
151
152 /**
153 * Modifies or sets a new value in an array by given path
154 *
155 * Example:
156 * - array:
157 * array(
158 * 'foo' => array(
159 * 'bar' => 42,
160 * ),
161 * );
162 * - path: foo/bar
163 * - value: 23
164 * - return:
165 * array(
166 * 'foo' => array(
167 * 'bar' => 23,
168 * ),
169 * );
170 *
171 * @param array $array Input array to manipulate
172 * @param string $path Path in array to search for
173 * @param mixed $value Value to set at path location in array
174 * @param string $delimiter Path delimiter
175 * @return array Modified array
176 * @throws \RuntimeException
177 */
178 public static function setValueByPath(array $array, $path, $value, $delimiter = '/')
179 {
180 if (empty($path)) {
181 throw new \RuntimeException('Path must not be empty', 1341406194);
182 }
183 if (!is_string($path)) {
184 throw new \RuntimeException('Path must be a string', 1341406402);
185 }
186 // Extract parts of the path
187 $path = str_getcsv($path, $delimiter);
188 // Point to the root of the array
189 $pointer = &$array;
190 // Find path in given array
191 foreach ($path as $segment) {
192 // Fail if the part is empty
193 if (empty($segment)) {
194 throw new \RuntimeException('Invalid path segment specified', 1341406846);
195 }
196 // Create cell if it doesn't exist
197 if (!array_key_exists($segment, $pointer)) {
198 $pointer[$segment] = array();
199 }
200 // Set pointer to new cell
201 $pointer = &$pointer[$segment];
202 }
203 // Set value of target cell
204 $pointer = $value;
205 return $array;
206 }
207
208 /**
209 * Remove a sub part from an array specified by path
210 *
211 * @param array $array Input array to manipulate
212 * @param string $path Path to remove from array
213 * @param string $delimiter Path delimiter
214 * @return array Modified array
215 * @throws \RuntimeException
216 */
217 public static function removeByPath(array $array, $path, $delimiter = '/')
218 {
219 if (empty($path)) {
220 throw new \RuntimeException('Path must not be empty', 1371757718);
221 }
222 if (!is_string($path)) {
223 throw new \RuntimeException('Path must be a string', 1371757719);
224 }
225 // Extract parts of the path
226 $path = str_getcsv($path, $delimiter);
227 $pathDepth = count($path);
228 $currentDepth = 0;
229 $pointer = &$array;
230 // Find path in given array
231 foreach ($path as $segment) {
232 $currentDepth++;
233 // Fail if the part is empty
234 if (empty($segment)) {
235 throw new \RuntimeException('Invalid path segment specified', 1371757720);
236 }
237 if (!array_key_exists($segment, $pointer)) {
238 throw new \RuntimeException('Path segment ' . $segment . ' does not exist in array', 1371758436);
239 }
240 if ($currentDepth === $pathDepth) {
241 unset($pointer[$segment]);
242 } else {
243 $pointer = &$pointer[$segment];
244 }
245 }
246 return $array;
247 }
248
249 /**
250 * Sorts an array recursively by key
251 *
252 * @param $array Array to sort recursively by key
253 * @return array Sorted array
254 */
255 public static function sortByKeyRecursive(array $array)
256 {
257 ksort($array);
258 foreach ($array as $key => $value) {
259 if (is_array($value) && !empty($value)) {
260 $array[$key] = self::sortByKeyRecursive($value);
261 }
262 }
263 return $array;
264 }
265
266 /**
267 * Sort an array of arrays by a given key using uasort
268 *
269 * @param array $arrays Array of arrays to sort
270 * @param string $key Key to sort after
271 * @param bool $ascending Set to TRUE for ascending order, FALSE for descending order
272 * @return array Array of sorted arrays
273 * @throws \RuntimeException
274 */
275 public static function sortArraysByKey(array $arrays, $key, $ascending = true)
276 {
277 if (empty($arrays)) {
278 return $arrays;
279 }
280 $sortResult = uasort($arrays, function (array $a, array $b) use ($key, $ascending) {
281 if (!isset($a[$key]) || !isset($b[$key])) {
282 throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
283 }
284 return ($ascending) ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
285 });
286 if (!$sortResult) {
287 throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
288 }
289 return $arrays;
290 }
291
292 /**
293 * Exports an array as string.
294 * Similar to var_export(), but representation follows the PSR-2 and TYPO3 core CGL.
295 *
296 * See unit tests for detailed examples
297 *
298 * @param array $array Array to export
299 * @param int $level Internal level used for recursion, do *not* set from outside!
300 * @return string String representation of array
301 * @throws \RuntimeException
302 */
303 public static function arrayExport(array $array = array(), $level = 0)
304 {
305 $lines = '[' . LF;
306 $level++;
307 $writeKeyIndex = false;
308 $expectedKeyIndex = 0;
309 foreach ($array as $key => $value) {
310 if ($key === $expectedKeyIndex) {
311 $expectedKeyIndex++;
312 } else {
313 // Found a non integer or non consecutive key, so we can break here
314 $writeKeyIndex = true;
315 break;
316 }
317 }
318 foreach ($array as $key => $value) {
319 // Indention
320 $lines .= str_repeat(' ', $level);
321 if ($writeKeyIndex) {
322 // Numeric / string keys
323 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
324 }
325 if (is_array($value)) {
326 if (!empty($value)) {
327 $lines .= self::arrayExport($value, $level);
328 } else {
329 $lines .= '[],' . LF;
330 }
331 } elseif (is_int($value) || is_float($value)) {
332 $lines .= $value . ',' . LF;
333 } elseif (is_null($value)) {
334 $lines .= 'null' . ',' . LF;
335 } elseif (is_bool($value)) {
336 $lines .= $value ? 'true' : 'false';
337 $lines .= ',' . LF;
338 } elseif (is_string($value)) {
339 // Quote \ to \\
340 $stringContent = str_replace('\\', '\\\\', $value);
341 // Quote ' to \'
342 $stringContent = str_replace('\'', '\\\'', $stringContent);
343 $lines .= '\'' . $stringContent . '\'' . ',' . LF;
344 } else {
345 throw new \RuntimeException('Objects are not supported', 1342294987);
346 }
347 }
348 $lines .= str_repeat(' ', ($level - 1)) . ']' . ($level - 1 == 0 ? '' : ',' . LF);
349 return $lines;
350 }
351
352 /**
353 * Converts a multidimensional array to a flat representation.
354 *
355 * See unit tests for more details
356 *
357 * Example:
358 * - array:
359 * array(
360 * 'first.' => array(
361 * 'second' => 1
362 * )
363 * )
364 * - result:
365 * array(
366 * 'first.second' => 1
367 * )
368 *
369 * Example:
370 * - array:
371 * array(
372 * 'first' => array(
373 * 'second' => 1
374 * )
375 * )
376 * - result:
377 * array(
378 * 'first.second' => 1
379 * )
380 *
381 * @param array $array The (relative) array to be converted
382 * @param string $prefix The (relative) prefix to be used (e.g. 'section.')
383 * @return array
384 */
385 public static function flatten(array $array, $prefix = '')
386 {
387 $flatArray = array();
388 foreach ($array as $key => $value) {
389 // Ensure there is no trailling dot:
390 $key = rtrim($key, '.');
391 if (!is_array($value)) {
392 $flatArray[$prefix . $key] = $value;
393 } else {
394 $flatArray = array_merge($flatArray, self::flatten($value, $prefix . $key . '.'));
395 }
396 }
397 return $flatArray;
398 }
399
400 /**
401 * Determine the intersections between two arrays, recursively comparing keys
402 * A complete sub array of $source will be preserved, if the key exists in $mask.
403 *
404 * See unit tests for more examples and edge cases.
405 *
406 * Example:
407 * - source:
408 * array(
409 * 'key1' => 'bar',
410 * 'key2' => array(
411 * 'subkey1' => 'sub1',
412 * 'subkey2' => 'sub2',
413 * ),
414 * 'key3' => 'baz',
415 * )
416 * - mask:
417 * array(
418 * 'key1' => NULL,
419 * 'key2' => array(
420 * 'subkey1' => exists',
421 * ),
422 * )
423 * - return:
424 * array(
425 * 'key1' => 'bar',
426 * 'key2' => array(
427 * 'subkey1' => 'sub1',
428 * ),
429 * )
430 *
431 * @param array $source Source array
432 * @param array $mask Array that has the keys which should be kept in the source array
433 * @return array Keys which are present in both arrays with values of the source array
434 */
435 public static function intersectRecursive(array $source, array $mask = array())
436 {
437 $intersection = array();
438 foreach ($source as $key => $_) {
439 if (!array_key_exists($key, $mask)) {
440 continue;
441 }
442 if (is_array($source[$key]) && is_array($mask[$key])) {
443 $value = self::intersectRecursive($source[$key], $mask[$key]);
444 if (!empty($value)) {
445 $intersection[$key] = $value;
446 }
447 } else {
448 $intersection[$key] = $source[$key];
449 }
450 }
451 return $intersection;
452 }
453
454 /**
455 * Renumber the keys of an array to avoid leaps if keys are all numeric.
456 *
457 * Is called recursively for nested arrays.
458 *
459 * Example:
460 *
461 * Given
462 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three')
463 * as input, it will return
464 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three')
465 *
466 * Will treat keys string representations of number (ie. '1') equal to the
467 * numeric value (ie. 1).
468 *
469 * Example:
470 * Given
471 * array('0' => 'Zero', '1' => 'One' )
472 * it will return
473 * array(0 => 'Zero', 1 => 'One')
474 *
475 * @param array $array Input array
476 * @param int $level Internal level used for recursion, do *not* set from outside!
477 * @return array
478 */
479 public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = array(), $level = 0)
480 {
481 $level++;
482 $allKeysAreNumeric = true;
483 foreach ($array as $key => $_) {
484 if (is_numeric($key) === false) {
485 $allKeysAreNumeric = false;
486 break;
487 }
488 }
489 $renumberedArray = $array;
490 if ($allKeysAreNumeric === true) {
491 $renumberedArray = array_values($array);
492 }
493 foreach ($renumberedArray as $key => $value) {
494 if (is_array($value)) {
495 $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
496 }
497 }
498 return $renumberedArray;
499 }
500
501 /**
502 * Merges two arrays recursively and "binary safe" (integer keys are
503 * overridden as well), overruling similar values in the original array
504 * with the values of the overrule array.
505 * In case of identical keys, ie. keeping the values of the overrule array.
506 *
507 * This method takes the original array by reference for speed optimization with large arrays
508 *
509 * The differences to the existing PHP function array_merge_recursive() are:
510 * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
511 * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
512 * * Elements or the original array get overwritten if the same key is present in the overrule array.
513 *
514 * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
515 * @param array $overrule Overrule array, overruling the original array
516 * @param bool $addKeys If set to FALSE, keys that are NOT found in $original will not be set. Thus only existing value can/will be overruled from overrule array.
517 * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
518 * @param bool $enableUnsetFeature If set, special values "__UNSET" can be used in the overrule array in order to unset array keys in the original array.
519 * @return void
520 */
521 public static function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
522 {
523 foreach ($overrule as $key => $_) {
524 if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
525 unset($original[$key]);
526 continue;
527 }
528 if (isset($original[$key]) && is_array($original[$key])) {
529 if (is_array($overrule[$key])) {
530 self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
531 }
532 } elseif (
533 ($addKeys || isset($original[$key])) &&
534 ($includeEmptyValues || $overrule[$key])
535 ) {
536 $original[$key] = $overrule[$key];
537 }
538 }
539 // This line is kept for backward compatibility reasons.
540 reset($original);
541 }
542
543 /**
544 * Check if an string item exists in an array.
545 * Please note that the order of function parameters is reverse compared to the PHP function in_array()!!!
546 *
547 * Comparison to PHP in_array():
548 * -> $array = array(0, 1, 2, 3);
549 * -> variant_a := \TYPO3\CMS\Core\Utility\ArrayUtility::inArray($array, $needle)
550 * -> variant_b := in_array($needle, $array)
551 * -> variant_c := in_array($needle, $array, TRUE)
552 * +---------+-----------+-----------+-----------+
553 * | $needle | variant_a | variant_b | variant_c |
554 * +---------+-----------+-----------+-----------+
555 * | '1a' | FALSE | TRUE | FALSE |
556 * | '' | FALSE | TRUE | FALSE |
557 * | '0' | TRUE | TRUE | FALSE |
558 * | 0 | TRUE | TRUE | TRUE |
559 * +---------+-----------+-----------+-----------+
560 *
561 * @param array $in_array One-dimensional array of items
562 * @param string $item Item to check for
563 * @return bool TRUE if $item is in the one-dimensional array $in_array
564 */
565 public static function inArray(array $in_array, $item)
566 {
567 foreach ($in_array as $val) {
568 if (!is_array($val) && (string)$val === (string)$item) {
569 return true;
570 }
571 }
572 return false;
573 }
574
575 /**
576 * Removes the value $cmpValue from the $array if found there. Returns the modified array
577 *
578 * @param array $array Array containing the values
579 * @param string $cmpValue Value to search for and if found remove array entry where found.
580 * @return array Output array with entries removed if search string is found
581 */
582 public static function removeArrayEntryByValue(array $array, $cmpValue)
583 {
584 foreach ($array as $k => $v) {
585 if (is_array($v)) {
586 $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
587 } elseif ((string)$v === (string)$cmpValue) {
588 unset($array[$k]);
589 }
590 }
591 return $array;
592 }
593
594 /**
595 * Filters an array to reduce its elements to match the condition.
596 * The values in $keepItems can be optionally evaluated by a custom callback function.
597 *
598 * Example (arguments used to call this function):
599 * $array = array(
600 * array('aa' => array('first', 'second'),
601 * array('bb' => array('third', 'fourth'),
602 * array('cc' => array('fifth', 'sixth'),
603 * );
604 * $keepItems = array('third');
605 * $getValueFunc = function($value) { return $value[0]; }
606 *
607 * Returns:
608 * array(
609 * array('bb' => array('third', 'fourth'),
610 * )
611 *
612 * @param array $array The initial array to be filtered/reduced
613 * @param mixed $keepItems The items which are allowed/kept in the array - accepts array or csv string
614 * @param string $getValueFunc (optional) Callback function used to get the value to keep
615 * @return array The filtered/reduced array with the kept items
616 */
617 public static function keepItemsInArray(array $array, $keepItems, $getValueFunc = null)
618 {
619 if ($array) {
620 // Convert strings to arrays:
621 if (is_string($keepItems)) {
622 $keepItems = GeneralUtility::trimExplode(',', $keepItems);
623 }
624 // Check if valueFunc can be executed:
625 if (!is_callable($getValueFunc)) {
626 $getValueFunc = null;
627 }
628 // Do the filtering:
629 if (is_array($keepItems) && !empty($keepItems)) {
630 foreach ($array as $key => $value) {
631 // Get the value to compare by using the callback function:
632 $keepValue = isset($getValueFunc) ? call_user_func($getValueFunc, $value) : $value;
633 if (!in_array($keepValue, $keepItems)) {
634 unset($array[$key]);
635 }
636 }
637 }
638 }
639 return $array;
640 }
641
642 /**
643 * Rename Array keys with a given mapping table
644 *
645 * @param array $array Array by reference which should be remapped
646 * @param array $mappingTable Array with remap information, array/$oldKey => $newKey)
647 */
648 public static function remapArrayKeys(array &$array, array $mappingTable)
649 {
650 foreach ($mappingTable as $old => $new) {
651 if ($new && isset($array[$old])) {
652 $array[$new] = $array[$old];
653 unset($array[$old]);
654 }
655 }
656 }
657
658 /**
659 * Filters keys off from first array that also exist in second array. Comparison is done by keys.
660 * This method is a recursive version of php array_diff_assoc()
661 *
662 * @param array $array1 Source array
663 * @param array $array2 Reduce source array by this array
664 * @return array Source array reduced by keys also present in second array
665 */
666 public static function arrayDiffAssocRecursive(array $array1, array $array2)
667 {
668 $differenceArray = array();
669 foreach ($array1 as $key => $value) {
670 if (!array_key_exists($key, $array2)) {
671 $differenceArray[$key] = $value;
672 } elseif (is_array($value)) {
673 if (is_array($array2[$key])) {
674 $differenceArray[$key] = self::arrayDiffAssocRecursive($value, $array2[$key]);
675 }
676 }
677 }
678 return $differenceArray;
679 }
680
681 /**
682 * Sorts an array by key recursive - uses natural sort order (aAbB-zZ)
683 *
684 * @param array $array array to be sorted recursively, passed by reference
685 * @return bool always TRUE
686 */
687 public static function naturalKeySortRecursive(array &$array)
688 {
689 uksort($array, 'strnatcasecmp');
690 foreach ($array as $key => &$value) {
691 if (is_array($value)) {
692 self::naturalKeySortRecursive($value);
693 }
694 }
695
696 return true;
697 }
698 }