1<?php
2namespace 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
17use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
18
19/**
20 * Class with helper functions for array handling
21 */
22class 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 (is_array($value) && 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 = "[\n";
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 .= "[],\n";
424                }
425            } elseif (is_int($value) || is_float($value)) {
426                $lines .= $value . ",\n";
427            } elseif ($value === null) {
428                $lines .= "null,\n";
429            } elseif (is_bool($value)) {
430                $lines .= $value ? 'true' : 'false';
431                $lines .= ",\n";
432            } elseif (is_string($value)) {
433                // Quote \ to \\
434                // Quote ' to \'
435                $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value);
436                $lines .= '\'' . $stringContent . "',\n";
437            } else {
438                throw new \RuntimeException('Objects are not supported', 1342294987);
439            }
440        }
441        $lines .= str_repeat('    ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ",\n");
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     * @param bool $keepDots
477     * @return array
478     */
479    public static function flatten(array $array, $prefix = '')
480    {
481        $flatArray = [];
482        foreach ($array as $key => $value) {
483            // Ensure there is no trailing dot:
484            $key = rtrim($key, '.');
485            if (!is_array($value)) {
486                $flatArray[$prefix . $key] = $value;
487            } else {
488                $flatArray = array_merge($flatArray, self::flatten($value, $prefix . $key . '.'));
489            }
490        }
491        return $flatArray;
492    }
493
494    /**
495     * Determine the intersections between two arrays, recursively comparing keys
496     * A complete sub array of $source will be preserved, if the key exists in $mask.
497     *
498     * See unit tests for more examples and edge cases.
499     *
500     * Example:
501     * - source:
502     * array(
503     *   'key1' => 'bar',
504     *   'key2' => array(
505     *     'subkey1' => 'sub1',
506     *     'subkey2' => 'sub2',
507     *   ),
508     *   'key3' => 'baz',
509     * )
510     * - mask:
511     * array(
512     *   'key1' => NULL,
513     *   'key2' => array(
514     *     'subkey1' => exists',
515     *   ),
516     * )
517     * - return:
518     * array(
519     *   'key1' => 'bar',
520     *   'key2' => array(
521     *     'subkey1' => 'sub1',
522     *   ),
523     * )
524     *
525     * @param array $source Source array
526     * @param array $mask Array that has the keys which should be kept in the source array
527     * @return array Keys which are present in both arrays with values of the source array
528     */
529    public static function intersectRecursive(array $source, array $mask = [])
530    {
531        $intersection = [];
532        foreach ($source as $key => $_) {
533            if (!array_key_exists($key, $mask)) {
534                continue;
535            }
536            if (is_array($source[$key]) && is_array($mask[$key])) {
537                $value = self::intersectRecursive($source[$key], $mask[$key]);
538                if (!empty($value)) {
539                    $intersection[$key] = $value;
540                }
541            } else {
542                $intersection[$key] = $source[$key];
543            }
544        }
545        return $intersection;
546    }
547
548    /**
549     * Renumber the keys of an array to avoid leaps if keys are all numeric.
550     *
551     * Is called recursively for nested arrays.
552     *
553     * Example:
554     *
555     * Given
556     *  array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three')
557     * as input, it will return
558     *  array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three')
559     *
560     * Will treat keys string representations of number (ie. '1') equal to the
561     * numeric value (ie. 1).
562     *
563     * Example:
564     * Given
565     *  array('0' => 'Zero', '1' => 'One' )
566     * it will return
567     *  array(0 => 'Zero', 1 => 'One')
568     *
569     * @param array $array Input array
570     * @param int $level Internal level used for recursion, do *not* set from outside!
571     * @return array
572     */
573    public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], $level = 0)
574    {
575        $level++;
576        $allKeysAreNumeric = true;
577        foreach ($array as $key => $_) {
578            if (is_int($key) === false) {
579                $allKeysAreNumeric = false;
580                break;
581            }
582        }
583        $renumberedArray = $array;
584        if ($allKeysAreNumeric === true) {
585            $renumberedArray = array_values($array);
586        }
587        foreach ($renumberedArray as $key => $value) {
588            if (is_array($value)) {
589                $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
590            }
591        }
592        return $renumberedArray;
593    }
594
595    /**
596     * Merges two arrays recursively and "binary safe" (integer keys are
597     * overridden as well), overruling similar values in the original array
598     * with the values of the overrule array.
599     * In case of identical keys, ie. keeping the values of the overrule array.
600     *
601     * This method takes the original array by reference for speed optimization with large arrays
602     *
603     * The differences to the existing PHP function array_merge_recursive() are:
604     *  * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
605     *  * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
606     *  * Elements or the original array get overwritten if the same key is present in the overrule array.
607     *
608     * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
609     * @param array $overrule Overrule array, overruling the original array
610     * @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.
611     * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
612     * @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.
613     */
614    public static function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
615    {
616        foreach ($overrule as $key => $_) {
617            if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
618                unset($original[$key]);
619                continue;
620            }
621            if (isset($original[$key]) && is_array($original[$key])) {
622                if (is_array($overrule[$key])) {
623                    self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
624                }
625            } elseif (
626                ($addKeys || isset($original[$key])) &&
627                ($includeEmptyValues || $overrule[$key])
628            ) {
629                $original[$key] = $overrule[$key];
630            }
631        }
632        // This line is kept for backward compatibility reasons.
633        reset($original);
634    }
635
636    /**
637     * Removes the value $cmpValue from the $array if found there. Returns the modified array
638     *
639     * @param array $array Array containing the values
640     * @param string $cmpValue Value to search for and if found remove array entry where found.
641     * @return array Output array with entries removed if search string is found
642     */
643    public static function removeArrayEntryByValue(array $array, $cmpValue)
644    {
645        foreach ($array as $k => $v) {
646            if (is_array($v)) {
647                $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
648            } elseif ((string)$v === (string)$cmpValue) {
649                unset($array[$k]);
650            }
651        }
652        return $array;
653    }
654
655    /**
656     * Filters an array to reduce its elements to match the condition.
657     * The values in $keepItems can be optionally evaluated by a custom callback function.
658     *
659     * Example (arguments used to call this function):
660     * $array = array(
661     * array('aa' => array('first', 'second'),
662     * array('bb' => array('third', 'fourth'),
663     * array('cc' => array('fifth', 'sixth'),
664     * );
665     * $keepItems = array('third');
666     * $getValueFunc = function($value) { return $value[0]; }
667     *
668     * Returns:
669     * array(
670     * array('bb' => array('third', 'fourth'),
671     * )
672     *
673     * @param array $array The initial array to be filtered/reduced
674     * @param mixed $keepItems The items which are allowed/kept in the array - accepts array or csv string
675     * @param string $getValueFunc (optional) Callback function used to get the value to keep
676     * @return array The filtered/reduced array with the kept items
677     */
678    public static function keepItemsInArray(array $array, $keepItems, $getValueFunc = null)
679    {
680        if ($array) {
681            // Convert strings to arrays:
682            if (is_string($keepItems)) {
683                $keepItems = GeneralUtility::trimExplode(',', $keepItems);
684            }
685            // Check if valueFunc can be executed:
686            if (!is_callable($getValueFunc)) {
687                $getValueFunc = null;
688            }
689            // Do the filtering:
690            if (is_array($keepItems) && !empty($keepItems)) {
691                $keepItems = array_flip($keepItems);
692                foreach ($array as $key => $value) {
693                    // Get the value to compare by using the callback function:
694                    $keepValue = isset($getValueFunc) ? call_user_func($getValueFunc, $value) : $value;
695                    if (!isset($keepItems[$keepValue])) {
696                        unset($array[$key]);
697                    }
698                }
699            }
700        }
701        return $array;
702    }
703
704    /**
705     * Rename Array keys with a given mapping table
706     *
707     * @param array	$array Array by reference which should be remapped
708     * @param array	$mappingTable Array with remap information, array/$oldKey => $newKey)
709     */
710    public static function remapArrayKeys(array &$array, array $mappingTable)
711    {
712        foreach ($mappingTable as $old => $new) {
713            if ($new && isset($array[$old])) {
714                $array[$new] = $array[$old];
715                unset($array[$old]);
716            }
717        }
718    }
719
720    /**
721     * Filters keys off from first array that also exist in second array. Comparison is done by keys.
722     * This method is a recursive version of php array_diff_key()
723     *
724     * @param array $array1 Source array
725     * @param array $array2 Reduce source array by this array
726     * @return array Source array reduced by keys also present in second array
727     */
728    public static function arrayDiffAssocRecursive(array $array1, array $array2)
729    {
730        $differenceArray = [];
731        foreach ($array1 as $key => $value) {
732            if (!array_key_exists($key, $array2)) {
733                $differenceArray[$key] = $value;
734            } elseif (is_array($value)) {
735                if (is_array($array2[$key])) {
736                    $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key]);
737                    if (!empty($recursiveResult)) {
738                        $differenceArray[$key] = $recursiveResult;
739                    }
740                }
741            }
742        }
743        return $differenceArray;
744    }
745
746    /**
747     * Sorts an array by key recursive - uses natural sort order (aAbB-zZ)
748     *
749     * @param array $array array to be sorted recursively, passed by reference
750     * @return bool always TRUE
751     */
752    public static function naturalKeySortRecursive(array &$array)
753    {
754        uksort($array, 'strnatcasecmp');
755        foreach ($array as $key => &$value) {
756            if (is_array($value)) {
757                self::naturalKeySortRecursive($value);
758            }
759        }
760
761        return true;
762    }
763
764    /**
765     * 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.
766     *
767     * @param array $setupArr TypoScript array with numerical array in
768     * @param bool $acceptAnyKeys If set, then a value is not required - the properties alone will be enough.
769     * @return array An array with all integer properties listed in numeric order.
770     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGet(), \TYPO3\CMS\Frontend\Imaging\GifBuilder, \TYPO3\CMS\Frontend\ContentObject\Menu\ImageMenuContentObject::makeImageMap()
771     */
772    public static function filterAndSortByNumericKeys($setupArr, $acceptAnyKeys = false)
773    {
774        $filteredKeys = [];
775        $keys = array_keys($setupArr);
776        foreach ($keys as $key) {
777            if ($acceptAnyKeys || MathUtility::canBeInterpretedAsInteger($key)) {
778                $filteredKeys[] = (int)$key;
779            }
780        }
781        $filteredKeys = array_unique($filteredKeys);
782        sort($filteredKeys);
783        return $filteredKeys;
784    }
785
786    /**
787     * If the array contains numerical keys only, sort it in ascending order
788     *
789     * @param array $array
790     *
791     * @return array
792     */
793    public static function sortArrayWithIntegerKeys(array $array)
794    {
795        if (count(array_filter(array_keys($array), 'is_string')) === 0) {
796            ksort($array);
797        }
798        return $array;
799    }
800
801    /**
802     * Sort keys from the current nesting level if all keys within the
803     * current nesting level are integers.
804     *
805     * @param array $array
806     * @return array
807     */
808    public static function sortArrayWithIntegerKeysRecursive(array $array): array
809    {
810        $array = static::sortArrayWithIntegerKeys($array);
811        foreach ($array as $key => $value) {
812            if (is_array($value) && !empty($value)) {
813                $array[$key] = self::sortArrayWithIntegerKeysRecursive($value);
814            }
815        }
816        return $array;
817    }
818
819    /**
820     * Recursively translate values.
821     *
822     * @param array $array
823     * @return array the modified array
824     */
825    public static function stripTagsFromValuesRecursive(array $array): array
826    {
827        $result = $array;
828        foreach ($result as $key => $value) {
829            if (is_array($value)) {
830                $result[$key] = self::stripTagsFromValuesRecursive($value);
831            } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
832                $result[$key] = strip_tags($value);
833            }
834        }
835        return $result;
836    }
837
838    /**
839     * Recursively filter an array
840     *
841     * @param array $array
842     * @param callable|null $callback
843     * @return array the filtered array
844     * @see https://secure.php.net/manual/en/function.array-filter.php
845     */
846    public static function filterRecursive(array $array, callable $callback = null): array
847    {
848        $callback = $callback ?: function ($value) {
849            return (bool)$value;
850        };
851
852        foreach ($array as $key => $value) {
853            if (is_array($value)) {
854                $array[$key] = self::filterRecursive($value, $callback);
855            }
856
857            if (!call_user_func($callback, $value)) {
858                unset($array[$key]);
859            }
860        }
861
862        return $array;
863    }
864}
865