Revert "[TASK] Avoid slow array functions in loops"
[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 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
18
19 /**
20 * Class with helper functions for array handling
21 */
22 class ArrayUtility
23 {
24 /**
25 * Validates the given $arrayToTest by checking if an element is not in $allowedArrayKeys.
26 *
27 * @param array $arrayToTest
28 * @param array $allowedArrayKeys
29 * @throws \InvalidArgumentException if an element in $arrayToTest is not in $allowedArrayKeys
30 * @internal
31 */
32 public static function assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys)
33 {
34 $notAllowedArrayKeys = array_keys(array_diff_key($arrayToTest, array_flip($allowedArrayKeys)));
35 if (count($notAllowedArrayKeys) !== 0) {
36 throw new \InvalidArgumentException(
37 sprintf(
38 'The options "%s" were not allowed (allowed were: "%s")',
39 implode(', ', $notAllowedArrayKeys),
40 implode(', ', $allowedArrayKeys)
41 ),
42 1325697085
43 );
44 }
45 }
46
47 /**
48 * Recursively convert 'true' and 'false' strings to boolean values.
49 *
50 * @param array $array
51 * @return array the modified array
52 */
53 public static function convertBooleanStringsToBooleanRecursive(array $array): array
54 {
55 $result = $array;
56 foreach ($result as $key => $value) {
57 if (is_array($value)) {
58 $result[$key] = self::convertBooleanStringsToBooleanRecursive($value);
59 } else {
60 if ($value === 'true') {
61 $result[$key] = true;
62 } elseif ($value === 'false') {
63 $result[$key] = false;
64 }
65 }
66 }
67 return $result;
68 }
69
70 /**
71 * Reduce an array by a search value and keep the array structure.
72 *
73 * Comparison is type strict:
74 * - For a given needle of type string, integer, array or boolean,
75 * value and value type must match to occur in result array
76 * - For a given object, an object within the array must be a reference to
77 * the same object to match (not just different instance of same class)
78 *
79 * Example:
80 * - Needle: 'findMe'
81 * - Given array:
82 * array(
83 * 'foo' => 'noMatch',
84 * 'bar' => 'findMe',
85 * 'foobar => array(
86 * 'foo' => 'findMe',
87 * ),
88 * );
89 * - Result:
90 * array(
91 * 'bar' => 'findMe',
92 * 'foobar' => array(
93 * 'foo' => findMe',
94 * ),
95 * );
96 *
97 * See the unit tests for more examples and expected behaviour
98 *
99 * @param mixed $needle The value to search for
100 * @param array $haystack The array in which to search
101 * @return array $haystack array reduced matching $needle values
102 */
103 public static function filterByValueRecursive($needle = '', array $haystack = [])
104 {
105 $resultArray = [];
106 // Define a lambda function to be applied to all members of this array dimension
107 // Call recursive if current value is of type array
108 // Write to $resultArray (by reference!) if types and value match
109 $callback = function (&$value, $key) use ($needle, &$resultArray) {
110 if ($value === $needle) {
111 $resultArray[$key] = $value;
112 } elseif (is_array($value)) {
113 $subArrayMatches = static::filterByValueRecursive($needle, $value);
114 if (!empty($subArrayMatches)) {
115 $resultArray[$key] = $subArrayMatches;
116 }
117 }
118 };
119 // array_walk() is not affected by the internal pointers, no need to reset
120 array_walk($haystack, $callback);
121 // Pointers to result array are reset internally
122 return $resultArray;
123 }
124
125 /**
126 * Checks if a given path exists in array
127 *
128 * Example:
129 * - array:
130 * array(
131 * 'foo' => array(
132 * 'bar' = 'test',
133 * )
134 * );
135 * - path: 'foo/bar'
136 * - return: TRUE
137 *
138 * @param array $array Given array
139 * @param string $path Path to test, 'foo/bar/foobar'
140 * @param string $delimiter Delimiter for path, default /
141 * @return bool TRUE if path exists in array
142 */
143 public static function isValidPath(array $array, $path, $delimiter = '/')
144 {
145 $isValid = true;
146 try {
147 static::getValueByPath($array, $path, $delimiter);
148 } catch (MissingArrayPathException $e) {
149 $isValid = false;
150 }
151 return $isValid;
152 }
153
154 /**
155 * Returns a value by given path
156 *
157 * Example
158 * - array:
159 * array(
160 * 'foo' => array(
161 * 'bar' => array(
162 * 'baz' => 42
163 * )
164 * )
165 * );
166 * - path: foo/bar/baz
167 * - return: 42
168 *
169 * If a path segments contains a delimiter character, the path segment
170 * must be enclosed by " (double quote), see unit tests for details
171 *
172 * @param array $array Input array
173 * @param array|string $path Path within the array
174 * @param string $delimiter Defined path delimiter, default /
175 * @return mixed
176 * @throws \RuntimeException if the path is empty, or if the path does not exist
177 * @throws \InvalidArgumentException if the path is neither array nor string
178 */
179 public static function getValueByPath(array $array, $path, $delimiter = '/')
180 {
181 // Extract parts of the path
182 if (is_string($path)) {
183 if ($path === '') {
184 // Programming error has to be sanitized before calling the method -> global exception
185 throw new \RuntimeException('Path must not be empty', 1341397767);
186 }
187 $path = str_getcsv($path, $delimiter);
188 } elseif (!is_array($path)) {
189 // Programming error has to be sanitized before calling the method -> global exception
190 throw new \InvalidArgumentException('getValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1476557628);
191 }
192 // Loop through each part and extract its value
193 $value = $array;
194 foreach ($path as $segment) {
195 if (array_key_exists($segment, $value)) {
196 // Replace current value with child
197 $value = $value[$segment];
198 } else {
199 // Throw specific exception if there is no such path
200 throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1341397869);
201 }
202 }
203 return $value;
204 }
205
206 /**
207 * Reindex keys from the current nesting level if all keys within
208 * the current nesting level are integers.
209 *
210 * @param array $array
211 * @return array
212 */
213 public static function reIndexNumericArrayKeysRecursive(array $array): array
214 {
215 if (count(array_filter(array_keys($array), 'is_string')) === 0) {
216 $array = array_values($array);
217 }
218 foreach ($array as $key => $value) {
219 if (is_array($value) && !empty($value)) {
220 $array[$key] = self::reIndexNumericArrayKeysRecursive($value);
221 }
222 }
223 return $array;
224 }
225
226 /**
227 * Recursively remove keys if their value are NULL.
228 *
229 * @param array $array
230 * @return array the modified array
231 */
232 public static function removeNullValuesRecursive(array $array): array
233 {
234 $result = $array;
235 foreach ($result as $key => $value) {
236 if (is_array($value)) {
237 $result[$key] = self::removeNullValuesRecursive($value);
238 } elseif ($value === null) {
239 unset($result[$key]);
240 }
241 }
242 return $result;
243 }
244
245 /**
246 * Modifies or sets a new value in an array by given path
247 *
248 * Example:
249 * - array:
250 * array(
251 * 'foo' => array(
252 * 'bar' => 42,
253 * ),
254 * );
255 * - path: foo/bar
256 * - value: 23
257 * - return:
258 * array(
259 * 'foo' => array(
260 * 'bar' => 23,
261 * ),
262 * );
263 *
264 * @param array $array Input array to manipulate
265 * @param string|array $path Path in array to search for
266 * @param mixed $value Value to set at path location in array
267 * @param string $delimiter Path delimiter
268 * @return array Modified array
269 * @throws \RuntimeException
270 */
271 public static function setValueByPath(array $array, $path, $value, $delimiter = '/')
272 {
273 if (is_string($path)) {
274 if ($path === '') {
275 throw new \RuntimeException('Path must not be empty', 1341406194);
276 }
277 // Extract parts of the path
278 $path = str_getcsv($path, $delimiter);
279 } elseif (!is_array($path) && !$path instanceof \ArrayAccess) {
280 throw new \InvalidArgumentException('setValueByPath() expects $path to be string, array or an object implementing \\ArrayAccess, "' . (is_object($path) ? get_class($path) : gettype($path)) . '" given.', 1478781081);
281 }
282 // Point to the root of the array
283 $pointer = &$array;
284 // Find path in given array
285 foreach ($path as $segment) {
286 // Fail if the part is empty
287 if ($segment === '') {
288 throw new \RuntimeException('Invalid path segment specified', 1341406846);
289 }
290 // Create cell if it doesn't exist
291 if (!array_key_exists($segment, $pointer)) {
292 $pointer[$segment] = [];
293 }
294 // Set pointer to new cell
295 $pointer = &$pointer[$segment];
296 }
297 // Set value of target cell
298 $pointer = $value;
299 return $array;
300 }
301
302 /**
303 * Remove a sub part from an array specified by path
304 *
305 * @param array $array Input array to manipulate
306 * @param string $path Path to remove from array
307 * @param string $delimiter Path delimiter
308 * @return array Modified array
309 * @throws \RuntimeException
310 */
311 public static function removeByPath(array $array, $path, $delimiter = '/')
312 {
313 if (!is_string($path)) {
314 throw new \RuntimeException('Path must be a string', 1371757719);
315 }
316 if ($path === '') {
317 throw new \RuntimeException('Path must not be empty', 1371757718);
318 }
319 // Extract parts of the path
320 $path = str_getcsv($path, $delimiter);
321 $pathDepth = count($path);
322 $currentDepth = 0;
323 $pointer = &$array;
324 // Find path in given array
325 foreach ($path as $segment) {
326 $currentDepth++;
327 // Fail if the part is empty
328 if ($segment === '') {
329 throw new \RuntimeException('Invalid path segment specified', 1371757720);
330 }
331 if (!array_key_exists($segment, $pointer)) {
332 throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1371758436);
333 }
334 if ($currentDepth === $pathDepth) {
335 unset($pointer[$segment]);
336 } else {
337 $pointer = &$pointer[$segment];
338 }
339 }
340 return $array;
341 }
342
343 /**
344 * Sorts an array recursively by key
345 *
346 * @param array $array Array to sort recursively by key
347 * @return array Sorted array
348 */
349 public static function sortByKeyRecursive(array $array)
350 {
351 ksort($array);
352 foreach ($array as $key => $value) {
353 if (is_array($value) && !empty($value)) {
354 $array[$key] = self::sortByKeyRecursive($value);
355 }
356 }
357 return $array;
358 }
359
360 /**
361 * Sort an array of arrays by a given key using uasort
362 *
363 * @param array $arrays Array of arrays to sort
364 * @param string $key Key to sort after
365 * @param bool $ascending Set to TRUE for ascending order, FALSE for descending order
366 * @return array Array of sorted arrays
367 * @throws \RuntimeException
368 */
369 public static function sortArraysByKey(array $arrays, $key, $ascending = true)
370 {
371 if (empty($arrays)) {
372 return $arrays;
373 }
374 $sortResult = uasort($arrays, function (array $a, array $b) use ($key, $ascending) {
375 if (!isset($a[$key]) || !isset($b[$key])) {
376 throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
377 }
378 return $ascending ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
379 });
380 if (!$sortResult) {
381 throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
382 }
383 return $arrays;
384 }
385
386 /**
387 * Exports an array as string.
388 * Similar to var_export(), but representation follows the PSR-2 and TYPO3 core CGL.
389 *
390 * See unit tests for detailed examples
391 *
392 * @param array $array Array to export
393 * @param int $level Internal level used for recursion, do *not* set from outside!
394 * @return string String representation of array
395 * @throws \RuntimeException
396 */
397 public static function arrayExport(array $array = [], $level = 0)
398 {
399 $lines = '[' . LF;
400 $level++;
401 $writeKeyIndex = false;
402 $expectedKeyIndex = 0;
403 foreach ($array as $key => $value) {
404 if ($key === $expectedKeyIndex) {
405 $expectedKeyIndex++;
406 } else {
407 // Found a non integer or non consecutive key, so we can break here
408 $writeKeyIndex = true;
409 break;
410 }
411 }
412 foreach ($array as $key => $value) {
413 // Indention
414 $lines .= str_repeat(' ', $level);
415 if ($writeKeyIndex) {
416 // Numeric / string keys
417 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
418 }
419 if (is_array($value)) {
420 if (!empty($value)) {
421 $lines .= self::arrayExport($value, $level);
422 } else {
423 $lines .= '[],' . LF;
424 }
425 } elseif (is_int($value) || is_float($value)) {
426 $lines .= $value . ',' . LF;
427 } elseif ($value === null) {
428 $lines .= 'null,' . LF;
429 } elseif (is_bool($value)) {
430 $lines .= $value ? 'true' : 'false';
431 $lines .= ',' . LF;
432 } elseif (is_string($value)) {
433 // Quote \ to \\
434 // Quote ' to \'
435 $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value);
436 $lines .= '\'' . $stringContent . '\',' . LF;
437 } else {
438 throw new \RuntimeException('Objects are not supported', 1342294987);
439 }
440 }
441 $lines .= str_repeat(' ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ',' . LF);
442 return $lines;
443 }
444
445 /**
446 * Converts a multidimensional array to a flat representation.
447 *
448 * See unit tests for more details
449 *
450 * Example:
451 * - array:
452 * array(
453 * 'first.' => array(
454 * 'second' => 1
455 * )
456 * )
457 * - result:
458 * array(
459 * 'first.second' => 1
460 * )
461 *
462 * Example:
463 * - array:
464 * array(
465 * 'first' => array(
466 * 'second' => 1
467 * )
468 * )
469 * - result:
470 * array(
471 * 'first.second' => 1
472 * )
473 *
474 * @param array $array The (relative) array to be converted
475 * @param string $prefix The (relative) prefix to be used (e.g. 'section.')
476 * @return array
477 */
478 public static function flatten(array $array, $prefix = '', bool $keepDots = false)
479 {
480 $flatArray = [];
481 foreach ($array as $key => $value) {
482 if ($keepDots === false) {
483 // Ensure there is no trailing dot:
484 $key = rtrim($key, '.');
485 }
486 if (!is_array($value)) {
487 $flatArray[$prefix . $key] = $value;
488 } else {
489 $newPrefix = $prefix . $key;
490 if ($keepDots === false) {
491 $newPrefix = $prefix . $key . '.';
492 }
493 $flatArray = array_merge($flatArray, self::flatten($value, $newPrefix, $keepDots));
494 }
495 }
496 return $flatArray;
497 }
498
499 /**
500 * Determine the intersections between two arrays, recursively comparing keys
501 * A complete sub array of $source will be preserved, if the key exists in $mask.
502 *
503 * See unit tests for more examples and edge cases.
504 *
505 * Example:
506 * - source:
507 * array(
508 * 'key1' => 'bar',
509 * 'key2' => array(
510 * 'subkey1' => 'sub1',
511 * 'subkey2' => 'sub2',
512 * ),
513 * 'key3' => 'baz',
514 * )
515 * - mask:
516 * array(
517 * 'key1' => NULL,
518 * 'key2' => array(
519 * 'subkey1' => exists',
520 * ),
521 * )
522 * - return:
523 * array(
524 * 'key1' => 'bar',
525 * 'key2' => array(
526 * 'subkey1' => 'sub1',
527 * ),
528 * )
529 *
530 * @param array $source Source array
531 * @param array $mask Array that has the keys which should be kept in the source array
532 * @return array Keys which are present in both arrays with values of the source array
533 */
534 public static function intersectRecursive(array $source, array $mask = [])
535 {
536 $intersection = [];
537 foreach ($source as $key => $_) {
538 if (!array_key_exists($key, $mask)) {
539 continue;
540 }
541 if (is_array($source[$key]) && is_array($mask[$key])) {
542 $value = self::intersectRecursive($source[$key], $mask[$key]);
543 if (!empty($value)) {
544 $intersection[$key] = $value;
545 }
546 } else {
547 $intersection[$key] = $source[$key];
548 }
549 }
550 return $intersection;
551 }
552
553 /**
554 * Renumber the keys of an array to avoid leaps if keys are all numeric.
555 *
556 * Is called recursively for nested arrays.
557 *
558 * Example:
559 *
560 * Given
561 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three')
562 * as input, it will return
563 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three')
564 *
565 * Will treat keys string representations of number (ie. '1') equal to the
566 * numeric value (ie. 1).
567 *
568 * Example:
569 * Given
570 * array('0' => 'Zero', '1' => 'One' )
571 * it will return
572 * array(0 => 'Zero', 1 => 'One')
573 *
574 * @param array $array Input array
575 * @param int $level Internal level used for recursion, do *not* set from outside!
576 * @return array
577 */
578 public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], $level = 0)
579 {
580 $level++;
581 $allKeysAreNumeric = true;
582 foreach ($array as $key => $_) {
583 if (is_int($key) === false) {
584 $allKeysAreNumeric = false;
585 break;
586 }
587 }
588 $renumberedArray = $array;
589 if ($allKeysAreNumeric === true) {
590 $renumberedArray = array_values($array);
591 }
592 foreach ($renumberedArray as $key => $value) {
593 if (is_array($value)) {
594 $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
595 }
596 }
597 return $renumberedArray;
598 }
599
600 /**
601 * Merges two arrays recursively and "binary safe" (integer keys are
602 * overridden as well), overruling similar values in the original array
603 * with the values of the overrule array.
604 * In case of identical keys, ie. keeping the values of the overrule array.
605 *
606 * This method takes the original array by reference for speed optimization with large arrays
607 *
608 * The differences to the existing PHP function array_merge_recursive() are:
609 * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
610 * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
611 * * Elements or the original array get overwritten if the same key is present in the overrule array.
612 *
613 * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
614 * @param array $overrule Overrule array, overruling the original array
615 * @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.
616 * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
617 * @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.
618 */
619 public static function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
620 {
621 foreach ($overrule as $key => $_) {
622 if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
623 unset($original[$key]);
624 continue;
625 }
626 if (isset($original[$key]) && is_array($original[$key])) {
627 if (is_array($overrule[$key])) {
628 self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
629 }
630 } elseif (
631 ($addKeys || isset($original[$key])) &&
632 ($includeEmptyValues || $overrule[$key])
633 ) {
634 $original[$key] = $overrule[$key];
635 }
636 }
637 // This line is kept for backward compatibility reasons.
638 reset($original);
639 }
640
641 /**
642 * Removes the value $cmpValue from the $array if found there. Returns the modified array
643 *
644 * @param array $array Array containing the values
645 * @param string $cmpValue Value to search for and if found remove array entry where found.
646 * @return array Output array with entries removed if search string is found
647 */
648 public static function removeArrayEntryByValue(array $array, $cmpValue)
649 {
650 foreach ($array as $k => $v) {
651 if (is_array($v)) {
652 $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
653 } elseif ((string)$v === (string)$cmpValue) {
654 unset($array[$k]);
655 }
656 }
657 return $array;
658 }
659
660 /**
661 * Filters an array to reduce its elements to match the condition.
662 * The values in $keepItems can be optionally evaluated by a custom callback function.
663 *
664 * Example (arguments used to call this function):
665 * $array = array(
666 * array('aa' => array('first', 'second'),
667 * array('bb' => array('third', 'fourth'),
668 * array('cc' => array('fifth', 'sixth'),
669 * );
670 * $keepItems = array('third');
671 * $getValueFunc = function($value) { return $value[0]; }
672 *
673 * Returns:
674 * array(
675 * array('bb' => array('third', 'fourth'),
676 * )
677 *
678 * @param array $array The initial array to be filtered/reduced
679 * @param mixed $keepItems The items which are allowed/kept in the array - accepts array or csv string
680 * @param string $getValueFunc (optional) Callback function used to get the value to keep
681 * @return array The filtered/reduced array with the kept items
682 */
683 public static function keepItemsInArray(array $array, $keepItems, $getValueFunc = null)
684 {
685 if ($array) {
686 // Convert strings to arrays:
687 if (is_string($keepItems)) {
688 $keepItems = GeneralUtility::trimExplode(',', $keepItems);
689 }
690 // Check if valueFunc can be executed:
691 if (!is_callable($getValueFunc)) {
692 $getValueFunc = null;
693 }
694 // Do the filtering:
695 if (is_array($keepItems) && !empty($keepItems)) {
696 $keepItems = array_flip($keepItems);
697 foreach ($array as $key => $value) {
698 // Get the value to compare by using the callback function:
699 $keepValue = isset($getValueFunc) ? call_user_func($getValueFunc, $value) : $value;
700 if (!isset($keepItems[$keepValue])) {
701 unset($array[$key]);
702 }
703 }
704 }
705 }
706 return $array;
707 }
708
709 /**
710 * Rename Array keys with a given mapping table
711 *
712 * @param array $array Array by reference which should be remapped
713 * @param array $mappingTable Array with remap information, array/$oldKey => $newKey)
714 */
715 public static function remapArrayKeys(array &$array, array $mappingTable)
716 {
717 foreach ($mappingTable as $old => $new) {
718 if ($new && isset($array[$old])) {
719 $array[$new] = $array[$old];
720 unset($array[$old]);
721 }
722 }
723 }
724
725 /**
726 * Filters keys off from first array that also exist in second array. Comparison is done by keys.
727 * This method is a recursive version of php array_diff_key()
728 *
729 * @param array $array1 Source array
730 * @param array $array2 Reduce source array by this array
731 * @return array Source array reduced by keys also present in second array
732 */
733 public static function arrayDiffAssocRecursive(array $array1, array $array2)
734 {
735 $differenceArray = [];
736 foreach ($array1 as $key => $value) {
737 if (!array_key_exists($key, $array2)) {
738 $differenceArray[$key] = $value;
739 } elseif (is_array($value)) {
740 if (is_array($array2[$key])) {
741 $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key]);
742 if (!empty($recursiveResult)) {
743 $differenceArray[$key] = $recursiveResult;
744 }
745 }
746 }
747 }
748 return $differenceArray;
749 }
750
751 /**
752 * Sorts an array by key recursive - uses natural sort order (aAbB-zZ)
753 *
754 * @param array $array array to be sorted recursively, passed by reference
755 * @return bool always TRUE
756 */
757 public static function naturalKeySortRecursive(array &$array)
758 {
759 uksort($array, 'strnatcasecmp');
760 foreach ($array as $key => &$value) {
761 if (is_array($value)) {
762 self::naturalKeySortRecursive($value);
763 }
764 }
765
766 return true;
767 }
768
769 /**
770 * Takes a TypoScript array as input and returns an array which contains all integer properties found which had a value (not only properties). The output array will be sorted numerically.
771 *
772 * @param array $setupArr TypoScript array with numerical array in
773 * @param bool $acceptAnyKeys If set, then a value is not required - the properties alone will be enough.
774 * @return array An array with all integer properties listed in numeric order.
775 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGet()
776 * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
777 */
778 public static function filterAndSortByNumericKeys($setupArr, $acceptAnyKeys = false)
779 {
780 $filteredKeys = [];
781 $keys = array_keys($setupArr);
782 foreach ($keys as $key) {
783 if ($acceptAnyKeys || MathUtility::canBeInterpretedAsInteger($key)) {
784 $filteredKeys[] = (int)$key;
785 }
786 }
787 $filteredKeys = array_unique($filteredKeys);
788 sort($filteredKeys);
789 return $filteredKeys;
790 }
791
792 /**
793 * If the array contains numerical keys only, sort it in ascending order
794 *
795 * @param array $array
796 *
797 * @return array
798 */
799 public static function sortArrayWithIntegerKeys(array $array)
800 {
801 if (count(array_filter(array_keys($array), 'is_string')) === 0) {
802 ksort($array);
803 }
804 return $array;
805 }
806
807 /**
808 * Sort keys from the current nesting level if all keys within the
809 * current nesting level are integers.
810 *
811 * @param array $array
812 * @return array
813 */
814 public static function sortArrayWithIntegerKeysRecursive(array $array): array
815 {
816 $array = static::sortArrayWithIntegerKeys($array);
817 foreach ($array as $key => $value) {
818 if (is_array($value) && !empty($value)) {
819 $array[$key] = self::sortArrayWithIntegerKeysRecursive($value);
820 }
821 }
822 return $array;
823 }
824
825 /**
826 * Recursively translate values.
827 *
828 * @param array $array
829 * @return array the modified array
830 */
831 public static function stripTagsFromValuesRecursive(array $array): array
832 {
833 $result = $array;
834 foreach ($result as $key => $value) {
835 if (is_array($value)) {
836 $result[$key] = self::stripTagsFromValuesRecursive($value);
837 } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
838 $result[$key] = strip_tags($value);
839 }
840 }
841 return $result;
842 }
843
844 /**
845 * Recursively filter an array
846 *
847 * @param array $array
848 * @param callable|null $callback
849 * @return array the filtered array
850 * @see https://secure.php.net/manual/en/function.array-filter.php
851 */
852 public static function filterRecursive(array $array, callable $callback = null): array
853 {
854 $callback = $callback ?: function ($value) {
855 return (bool)$value;
856 };
857
858 foreach ($array as $key => $value) {
859 if (is_array($value)) {
860 $array[$key] = self::filterRecursive($value, $callback);
861 }
862
863 if (!call_user_func($callback, $value)) {
864 unset($array[$key]);
865 }
866 }
867
868 return $array;
869 }
870
871 /**
872 * Check whether the array has non-integer keys. If there is at least one string key, $array will be
873 * regarded as an associative array.
874 *
875 * @param array $array
876 * @return bool True in case a string key was found.
877 * @internal
878 */
879 public static function isAssociative(array $array): bool
880 {
881 return count(array_filter(array_keys($array), 'is_string')) > 0;
882 }
883
884 /**
885 * Same as array_replace_recursive except that when in simple arrays (= YAML lists), the entries are
886 * appended (array_merge). The second array takes precedence in case of equal sub arrays.
887 *
888 * @param array $array1
889 * @param array $array2
890 * @return array
891 * @internal
892 */
893 public static function replaceAndAppendScalarValuesRecursive(array $array1, array $array2): array
894 {
895 // Simple lists get merged / added up
896 if (!self::isAssociative($array1)) {
897 return array_merge($array1, $array2);
898 }
899 foreach ($array1 as $k => $v) {
900 // The key also exists in second array, if it is a simple value
901 // then $array2 will override the value, where an array is calling
902 // replaceAndAppendScalarValuesRecursive() recursively.
903 if (isset($array2[$k])) {
904 if (is_array($v) && is_array($array2[$k])) {
905 $array1[$k] = self::replaceAndAppendScalarValuesRecursive($v, $array2[$k]);
906 } else {
907 $array1[$k] = $array2[$k];
908 }
909 unset($array2[$k]);
910 }
911 }
912 // If there are properties in the second array left, they are added up
913 if (!empty($array2)) {
914 foreach ($array2 as $k => $v) {
915 $array1[$k] = $v;
916 }
917 }
918
919 return $array1;
920 }
921 }