‪TYPO3CMS  ‪main
ArrayUtility.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
21 
26 {
33  public static function ‪assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys): void
34  {
35  $notAllowedArrayKeys = array_keys(array_diff_key($arrayToTest, array_flip($allowedArrayKeys)));
36  if (count($notAllowedArrayKeys) !== 0) {
37  throw new \InvalidArgumentException(
38  sprintf(
39  'The options "%s" were not allowed (allowed were: "%s")',
40  implode(', ', $notAllowedArrayKeys),
41  implode(', ', $allowedArrayKeys)
42  ),
43  1325697085
44  );
45  }
46  }
47 
51  public static function ‪convertBooleanStringsToBooleanRecursive(array $array): array
52  {
53  $result = $array;
54  foreach ($result as $key => $value) {
55  if (is_array($value)) {
57  } else {
58  if ($value === 'true') {
59  $result[$key] = true;
60  } elseif ($value === 'false') {
61  $result[$key] = false;
62  }
63  }
64  }
65  return $result;
66  }
67 
101  public static function ‪filterByValueRecursive(mixed $needle = '', array $haystack = []): array
102  {
103  $resultArray = [];
104  // Define a lambda function to be applied to all members of this array dimension
105  // Call recursive if current value is of type array
106  // Write to $resultArray (by reference!) if types and value match
107  $callback = static function (&$value, $key) use ($needle, &$resultArray) {
108  if ($value === $needle) {
109  $resultArray[$key] = $value;
110  } elseif (is_array($value)) {
111  $subArrayMatches = static::filterByValueRecursive($needle, $value);
112  if (!empty($subArrayMatches)) {
113  $resultArray[$key] = $subArrayMatches;
114  }
115  }
116  };
117  // array_walk() is not affected by the internal pointers, no need to reset
118  array_walk($haystack, $callback);
119  // Pointers to result array are reset internally
120  return $resultArray;
121  }
122 
141  public static function ‪isValidPath(array $array, array|string $path, string $delimiter = '/'): bool
142  {
143  $isValid = true;
144  try {
145  static::getValueByPath($array, $path, $delimiter);
146  } catch (‪MissingArrayPathException) {
147  $isValid = false;
148  }
149  return $isValid;
150  }
151 
176  public static function ‪getValueByPath(array $array, array|string $path, string $delimiter = '/'): mixed
177  {
178  // Upcast a string to an array if necessary
179  if (is_string($path)) {
180  if ($path === '') {
181  // Programming error has to be sanitized before calling the method -> global exception
182  throw new \RuntimeException('Path must not be empty', 1341397767);
183  }
184  $path = str_getcsv($path, $delimiter);
185  }
186  // Loop through each part and extract its value
187  $value = $array;
188  foreach ($path as $segment) {
189  if (is_array($value) && array_key_exists($segment, $value)) {
190  // Replace current value with child
191  $value = $value[$segment];
192  } else {
193  // Throw specific exception if there is no such path
194  throw new ‪MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1341397869);
195  }
196  }
197  return $value;
198  }
199 
204  public static function ‪reIndexNumericArrayKeysRecursive(array $array): array
205  {
206  // Can't use array_is_list() because an all-integers but non-sequential
207  // array is not a list, but should be reindexed.
208  if (count(array_filter(array_keys($array), is_string(...))) === 0) {
209  $array = array_values($array);
210  }
211  foreach ($array as $key => $value) {
212  if (is_array($value) && !empty($value)) {
213  $array[$key] = ‪self::reIndexNumericArrayKeysRecursive($value);
214  }
215  }
216  return $array;
217  }
218 
222  public static function ‪removeNullValuesRecursive(array $array): array
223  {
224  $result = $array;
225  foreach ($result as $key => $value) {
226  if (is_array($value)) {
227  $result[$key] = ‪self::removeNullValuesRecursive($value);
228  } elseif ($value === null) {
229  unset($result[$key]);
230  }
231  }
232  return $result;
233  }
234 
261  public static function ‪setValueByPath(array $array, string|array|\ArrayAccess $path, mixed $value, string $delimiter = '/'): array
262  {
263  if (is_string($path)) {
264  if ($path === '') {
265  throw new \RuntimeException('Path must not be empty', 1341406194);
266  }
267  // Extract parts of the path
268  $path = str_getcsv($path, $delimiter);
269  }
270  // Point to the root of the array
271  $pointer = &$array;
272  // Find path in given array
273  foreach ($path as $segment) {
274  // Fail if the part is empty
275  if ($segment === '') {
276  throw new \RuntimeException('Invalid path segment specified', 1341406846);
277  }
278  // Create cell if it doesn't exist
279  if (is_array($pointer) && !array_key_exists($segment, $pointer)) {
280  $pointer[$segment] = [];
281  }
282  // Make it array if it was something else before
283  if (!is_array($pointer)) {
284  $pointer = [];
285  }
286  // Set pointer to new cell
287  $pointer = &$pointer[$segment];
288  }
289  // Set value of target cell
290  $pointer = $value;
291  return $array;
292  }
293 
303  public static function ‪removeByPath(array $array, string $path, string $delimiter = '/'): array
304  {
305  if ($path === '') {
306  throw new \RuntimeException('Path must not be empty', 1371757718);
307  }
308  // Extract parts of the path
309  $pathSegments = str_getcsv($path, $delimiter);
310  $pathDepth = count($pathSegments);
311  $currentDepth = 0;
312  $pointer = &$array;
313  // Find path in given array
314  foreach ($pathSegments as $segment) {
315  $currentDepth++;
316  // Fail if the part is empty
317  if ($segment === '') {
318  throw new \RuntimeException('Invalid path segment specified', 1371757720);
319  }
320  if (!array_key_exists($segment, $pointer)) {
321  throw new ‪MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $pathSegments) . ' does not exist in array', 1371758436);
322  }
323  if ($currentDepth === $pathDepth) {
324  unset($pointer[$segment]);
325  } else {
326  $pointer = &$pointer[$segment];
327  }
328  }
329  return $array;
330  }
331 
338  public static function ‪sortByKeyRecursive(array $array): array
339  {
340  ksort($array);
341  foreach ($array as $key => $value) {
342  if (is_array($value) && !empty($value)) {
343  $array[$key] = ‪self::sortByKeyRecursive($value);
344  }
345  }
346  return $array;
347  }
348 
358  public static function ‪sortArraysByKey(array $arrays, string $key, bool $ascending = true): array
359  {
360  if (empty($arrays)) {
361  return $arrays;
362  }
363  $sortResult = uasort($arrays, static function (array $a, array $b) use ($key, $ascending) {
364  if (!isset($a[$key]) || !isset($b[$key])) {
365  throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
366  }
367  return $ascending ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
368  });
369  if (!$sortResult) {
370  throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
371  }
372  return $arrays;
373  }
374 
386  public static function ‪arrayExport(array $array = [], int $level = 0): string
387  {
388  $lines = "[\n";
389  $level++;
390  $writeKeyIndex = false;
391  $expectedKeyIndex = 0;
392  foreach ($array as $key => $value) {
393  if ($key === $expectedKeyIndex) {
394  $expectedKeyIndex++;
395  } else {
396  // Found a non-integer or non-consecutive key, so we can break here
397  $writeKeyIndex = true;
398  break;
399  }
400  }
401  foreach ($array as $key => $value) {
402  // Indention
403  $lines .= str_repeat(' ', $level);
404  if ($writeKeyIndex) {
405  // Numeric / string keys
406  $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
407  }
408  if (is_array($value)) {
409  if (!empty($value)) {
410  $lines .= self::arrayExport($value, $level);
411  } else {
412  $lines .= "[],\n";
413  }
414  } elseif (is_int($value) || is_float($value)) {
415  $lines .= $value . ",\n";
416  } elseif ($value === null) {
417  $lines .= "null,\n";
418  } elseif (is_bool($value)) {
419  $lines .= $value ? 'true' : 'false';
420  $lines .= ",\n";
421  } elseif (is_string($value)) {
422  // Quote \ to \\
423  // Quote ' to \'
424  $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value);
425  $lines .= '\'' . $stringContent . "',\n";
426  } else {
427  throw new \RuntimeException('Objects are not supported', 1342294987);
428  }
429  }
430  $lines .= str_repeat(' ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ",\n");
431  return $lines;
432  }
433 
469  public static function ‪flatten(array $array, string $prefix = '', bool $keepDots = false): array
470  {
471  $flatArray = [];
472  foreach ($array as $key => $value) {
473  if ($keepDots === false) {
474  // Ensure there is no trailing dot:
475  $key = rtrim((string)$key, '.');
476  }
477  if (!is_array($value)) {
478  $flatArray[$prefix . $key] = $value;
479  } else {
480  $newPrefix = $prefix . $key;
481  if ($keepDots === false) {
482  $newPrefix = $prefix . $key . '.';
483  }
484  $flatArray = array_merge($flatArray, self::flatten($value, $newPrefix, $keepDots));
485  }
486  }
487  return $flatArray;
488  }
489 
496  public static function ‪flattenPlain(array $array): array
497  {
498  $flattenRecursive = static function (array $array, string $prefix = '') use (&$flattenRecursive) {
499  $flatArray = [];
500  foreach ($array as $key => $value) {
501  $key = addcslashes((string)$key, '.');
502  if (!is_array($value)) {
503  $flatArray[] = [$prefix . $key => $value];
504  } else {
505  $flatArray[] = $flattenRecursive($value, $prefix . $key . '.');
506  }
507  }
508 
509  return array_merge(...$flatArray);
510  };
511 
512  return $flattenRecursive($array);
513  }
514 
535  public static function unflatten(array $input, string $delimiter = '.'): array
536  {
537  ‪$output = [];
538  foreach ($input as $key => $value) {
539  $parts = ‪StringUtility::explodeEscaped($delimiter, $key);
540  $nested = &‪$output;
541  while (count($parts) > 1) {
542  $nested = &$nested[array_shift($parts)];
543  if (!is_array($nested)) {
544  $nested = [];
545  }
546  }
547  $nested[array_shift($parts)] = $value;
548  }
549  return ‪$output;
550  }
551 
587  public static function intersectRecursive(array $source, array $mask = []): array
588  {
589  $intersection = [];
590  foreach ($source as $key => $_) {
591  if (!array_key_exists($key, $mask)) {
592  continue;
593  }
594  if (is_array($source[$key]) && is_array($mask[$key])) {
595  $value = self::intersectRecursive($source[$key], $mask[$key]);
596  if (!empty($value)) {
597  $intersection[$key] = $value;
598  }
599  } else {
600  $intersection[$key] = $source[$key];
601  }
602  }
603  return $intersection;
604  }
605 
630  public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], int $level = 0): array
631  {
632  $level++;
633  $allKeysAreNumeric = true;
634  foreach ($array as $key => $_) {
635  if (is_int($key) === false) {
636  $allKeysAreNumeric = false;
637  break;
638  }
639  }
640  $renumberedArray = $array;
641  if ($allKeysAreNumeric === true) {
642  $renumberedArray = array_values($array);
643  }
644  foreach ($renumberedArray as $key => $value) {
645  if (is_array($value)) {
646  $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
647  }
648  }
649  return $renumberedArray;
650  }
651 
671  public static function mergeRecursiveWithOverrule(array &$original, array $overrule, bool $addKeys = true, bool $includeEmptyValues = true, bool $enableUnsetFeature = true): void
672  {
673  foreach ($overrule as $key => $_) {
674  if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
675  unset($original[$key]);
676  continue;
677  }
678  if (isset($original[$key]) && is_array($original[$key])) {
679  if (is_array($overrule[$key])) {
680  self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
681  }
682  } elseif (
683  ($addKeys || isset($original[$key])) &&
684  ($includeEmptyValues || $overrule[$key])
685  ) {
686  $original[$key] = $overrule[$key];
687  }
688  }
689  // This line is kept for backward compatibility reasons.
690  reset($original);
691  }
692 
700  public static function removeArrayEntryByValue(array $array, string $cmpValue): array
701  {
702  foreach ($array as $k => $v) {
703  if (is_array($v)) {
704  $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
705  } elseif ((string)$v === $cmpValue) {
706  unset($array[$k]);
707  }
708  }
709  return $array;
710  }
711 
741  public static function keepItemsInArray(array $array, array|string|null $keepItems, ?callable $getValueFunc = null): array
742  {
743  if (empty($array)) {
744  return $array;
745  }
746 
747  // Convert strings to arrays:
748  if (is_string($keepItems)) {
749  $keepItems = ‪GeneralUtility::trimExplode(',', $keepItems);
750  }
751 
752  if (empty($keepItems)) {
753  return $array;
754  }
755 
756  // Check if valueFunc can be executed:
757  if (!is_callable($getValueFunc)) {
758  $getValueFunc = null;
759  }
760  // Do the filtering:
761  if (is_array($keepItems)) {
762  $keepItems = array_flip($keepItems);
763  foreach ($array as $key => $value) {
764  // Get the value to compare by using the callback function:
765  $keepValue = isset($getValueFunc) ? $getValueFunc($value) : $value;
766  if (!isset($keepItems[$keepValue])) {
767  unset($array[$key]);
768  }
769  }
770  }
771 
772  return $array;
773  }
774 
781  public static function remapArrayKeys(array &$array, array $mappingTable): void
782  {
783  foreach ($mappingTable as $old => $new) {
784  if ($new && isset($array[$old])) {
785  $array[$new] = $array[$old];
786  unset($array[$old]);
787  }
788  }
789  }
790 
799  public static function arrayDiffKeyRecursive(array $array1, array $array2): array
800  {
801  $differenceArray = [];
802  foreach ($array1 as $key => $value) {
803  if (!array_key_exists($key, $array2)) {
804  $differenceArray[$key] = $value;
805  } elseif (is_array($value)) {
806  if (is_array($array2[$key])) {
807  $recursiveResult = self::arrayDiffKeyRecursive($value, $array2[$key]);
808  if (!empty($recursiveResult)) {
809  $differenceArray[$key] = $recursiveResult;
810  }
811  }
812  }
813  }
814  return $differenceArray;
815  }
816 
825  public static function arrayDiffAssocRecursive(array $array1, array $array2): array
826  {
827  $differenceArray = [];
828  foreach ($array1 as $key => $value) {
829  if (!array_key_exists($key, $array2) || (!is_array($value) && $value !== $array2[$key])) {
830  $differenceArray[$key] = $value;
831  } elseif (is_array($value)) {
832  if (is_array($array2[$key])) {
833  $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key]);
834  if (!empty($recursiveResult)) {
835  $differenceArray[$key] = $recursiveResult;
836  }
837  }
838  }
839  }
840  return $differenceArray;
841  }
842 
849  public static function naturalKeySortRecursive(array &$array): bool
850  {
851  uksort($array, 'strnatcasecmp');
852  foreach ($array as $key => &$value) {
853  if (is_array($value)) {
854  self::naturalKeySortRecursive($value);
855  }
856  }
857 
858  return true;
859  }
860 
870  public static function filterAndSortByNumericKeys(array $setupArr, bool $acceptAnyKeys = false): array
871  {
872  $filteredKeys = [];
873  $keys = array_keys($setupArr);
874  foreach ($keys as $key) {
875  if ($acceptAnyKeys || ‪MathUtility::canBeInterpretedAsInteger($key)) {
876  $filteredKeys[] = (int)$key;
877  }
878  }
879  $filteredKeys = array_unique($filteredKeys);
880  sort($filteredKeys);
881  return $filteredKeys;
882  }
883 
887  public static function sortArrayWithIntegerKeys(array $array): array
888  {
889  // Can't use array_is_list() because an all-integers but non-sequential
890  // array is not a list, but can still be numerically sorted.
891  if (count(array_filter(array_keys($array), is_string(...))) === 0) {
892  ksort($array);
893  }
894  return $array;
895  }
896 
901  public static function sortArrayWithIntegerKeysRecursive(array $array): array
902  {
903  $array = static::sortArrayWithIntegerKeys($array);
904  foreach ($array as $key => $value) {
905  if (is_array($value) && !empty($value)) {
906  $array[$key] = self::sortArrayWithIntegerKeysRecursive($value);
907  }
908  }
909  return $array;
910  }
911 
915  public static function stripTagsFromValuesRecursive(array $array): array
916  {
917  $result = $array;
918  foreach ($result as $key => $value) {
919  if (is_array($value)) {
920  $result[$key] = self::stripTagsFromValuesRecursive($value);
921  } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
922  $result[$key] = strip_tags((string)$value);
923  }
924  }
925  return $result;
926  }
927 
941  public static function filterRecursive(array $array, callable $callback = null, int $mode = 0): array
942  {
943  $callback ??= static fn($value) => (bool)$value;
944 
945  foreach ($array as $key => $value) {
946  if (is_array($value)) {
947  $array[$key] = self::filterRecursive($value, $callback, $mode);
948  }
949  }
950  return array_filter($array, $callback, $mode);
951  }
952 
960  public static function ‪isAssociative(array $array): bool
961  {
962  return !array_is_list($array);
963  }
964 
971  public static function ‪replaceAndAppendScalarValuesRecursive(array $array1, array $array2): array
972  {
973  // Simple lists get merged / added up
974  if (array_is_list($array1)) {
975  return array_merge($array1, $array2);
976  }
977  foreach ($array1 as $k => $v) {
978  // The key also exists in second array, if it is a simple value
979  // then $array2 will override the value, where an array is calling
980  // replaceAndAppendScalarValuesRecursive() recursively.
981  if (isset($array2[$k])) {
982  if (is_array($v) && is_array($array2[$k])) {
983  $array1[$k] = ‪self::replaceAndAppendScalarValuesRecursive($v, $array2[$k]);
984  } else {
985  $array1[$k] = $array2[$k];
986  }
987  unset($array2[$k]);
988  }
989  }
990  // If there are properties in the second array left, they are added up
991  if (!empty($array2)) {
992  foreach ($array2 as $k => $v) {
993  $array1[$k] = $v;
994  }
995  }
996 
997  return $array1;
998  }
999 }
‪TYPO3\CMS\Core\Utility\ArrayUtility\isAssociative
‪static bool isAssociative(array $array)
Definition: ArrayUtility.php:960
‪TYPO3\CMS\Core\Utility\ArrayUtility\flattenPlain
‪static flattenPlain(array $array)
Definition: ArrayUtility.php:496
‪TYPO3\CMS\Core\Utility\ArrayUtility\removeByPath
‪static array removeByPath(array $array, string $path, string $delimiter='/')
Definition: ArrayUtility.php:303
‪TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException
Definition: MissingArrayPathException.php:27
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:18
‪TYPO3\CMS\Core\Utility\ArrayUtility\sortByKeyRecursive
‪static array sortByKeyRecursive(array $array)
Definition: ArrayUtility.php:338
‪TYPO3\CMS\Core\Utility\ArrayUtility\replaceAndAppendScalarValuesRecursive
‪static replaceAndAppendScalarValuesRecursive(array $array1, array $array2)
Definition: ArrayUtility.php:971
‪TYPO3\CMS\Core\Utility\ArrayUtility\arrayExport
‪static string arrayExport(array $array=[], int $level=0)
Definition: ArrayUtility.php:386
‪TYPO3\CMS\Core\Utility\ArrayUtility\isValidPath
‪static bool isValidPath(array $array, array|string $path, string $delimiter='/')
Definition: ArrayUtility.php:141
‪TYPO3\CMS\Core\Utility\ArrayUtility\getValueByPath
‪static getValueByPath(array $array, array|string $path, string $delimiter='/')
Definition: ArrayUtility.php:176
‪TYPO3\CMS\Core\Utility\ArrayUtility\flatten
‪static flatten(array $array, string $prefix='', bool $keepDots=false)
Definition: ArrayUtility.php:469
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Utility\ArrayUtility\removeNullValuesRecursive
‪static removeNullValuesRecursive(array $array)
Definition: ArrayUtility.php:222
‪$output
‪$output
Definition: annotationChecker.php:114
‪TYPO3\CMS\Core\Utility\ArrayUtility\filterByValueRecursive
‪static array filterByValueRecursive(mixed $needle='', array $haystack=[])
Definition: ArrayUtility.php:101
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\Utility\ArrayUtility\reIndexNumericArrayKeysRecursive
‪static reIndexNumericArrayKeysRecursive(array $array)
Definition: ArrayUtility.php:204
‪TYPO3\CMS\Core\Utility\ArrayUtility\assertAllArrayKeysAreValid
‪static assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys)
Definition: ArrayUtility.php:33
‪TYPO3\CMS\Core\Utility\ArrayUtility\sortArraysByKey
‪static array sortArraysByKey(array $arrays, string $key, bool $ascending=true)
Definition: ArrayUtility.php:358
‪TYPO3\CMS\Core\Utility\ArrayUtility\setValueByPath
‪static array setValueByPath(array $array, string|array|\ArrayAccess $path, mixed $value, string $delimiter='/')
Definition: ArrayUtility.php:261
‪TYPO3\CMS\Core\Utility\ArrayUtility\convertBooleanStringsToBooleanRecursive
‪static convertBooleanStringsToBooleanRecursive(array $array)
Definition: ArrayUtility.php:51
‪TYPO3\CMS\Core\Utility\StringUtility\explodeEscaped
‪static explodeEscaped(string $delimiter, string $subject, string $escapeCharacter='\\')
Definition: StringUtility.php:209
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:817