‪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], $b[$key])) {
365  throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
366  }
367  if (!is_scalar($a[$key])) {
368  throw new \RuntimeException(sprintf('The specified sorting key "%s" is not a scalar value, given "%s".', $key, gettype($a[$key])), 1373727310);
369  }
370  if (!is_scalar($b[$key])) {
371  throw new \RuntimeException(sprintf('The specified sorting key "%s" is not a scalar value, given "%s".', $key, gettype($b[$key])), 1373727311);
372  }
373  return $ascending ? strcasecmp((string)$a[$key], (string)$b[$key]) : strcasecmp((string)$b[$key], (string)$a[$key]);
374  });
375  if (!$sortResult) {
376  throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
377  }
378  return $arrays;
379  }
380 
392  public static function ‪arrayExport(array $array = [], int $level = 0): string
393  {
394  $lines = "[\n";
395  $level++;
396  $writeKeyIndex = false;
397  $expectedKeyIndex = 0;
398  foreach ($array as $key => $value) {
399  if ($key === $expectedKeyIndex) {
400  $expectedKeyIndex++;
401  } else {
402  // Found a non-integer or non-consecutive key, so we can break here
403  $writeKeyIndex = true;
404  break;
405  }
406  }
407  foreach ($array as $key => $value) {
408  // Indention
409  $lines .= str_repeat(' ', $level);
410  if ($writeKeyIndex) {
411  // Numeric / string keys
412  $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
413  }
414  if (is_array($value)) {
415  if (!empty($value)) {
416  $lines .= self::arrayExport($value, $level);
417  } else {
418  $lines .= "[],\n";
419  }
420  } elseif (is_int($value) || is_float($value)) {
421  $lines .= $value . ",\n";
422  } elseif ($value === null) {
423  $lines .= "null,\n";
424  } elseif (is_bool($value)) {
425  $lines .= $value ? 'true' : 'false';
426  $lines .= ",\n";
427  } elseif (is_string($value)) {
428  // Quote \ to \\
429  // Quote ' to \'
430  $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value);
431  $lines .= '\'' . $stringContent . "',\n";
432  } else {
433  throw new \RuntimeException('Objects are not supported', 1342294987);
434  }
435  }
436  $lines .= str_repeat(' ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ",\n");
437  return $lines;
438  }
439 
475  public static function ‪flatten(array $array, string $prefix = '', bool $keepDots = false): array
476  {
477  $flatArray = [];
478  foreach ($array as $key => $value) {
479  if ($keepDots === false) {
480  // Ensure there is no trailing dot:
481  $key = rtrim((string)$key, '.');
482  }
483  if (!is_array($value)) {
484  $flatArray[$prefix . $key] = $value;
485  } else {
486  $newPrefix = $prefix . $key;
487  if ($keepDots === false) {
488  $newPrefix = $prefix . $key . '.';
489  }
490  $flatArray = array_merge($flatArray, self::flatten($value, $newPrefix, $keepDots));
491  }
492  }
493  return $flatArray;
494  }
495 
502  public static function ‪flattenPlain(array $array): array
503  {
504  $flattenRecursive = static function (array $array, string $prefix = '') use (&$flattenRecursive) {
505  $flatArray = [];
506  foreach ($array as $key => $value) {
507  $key = addcslashes((string)$key, '.');
508  if (!is_array($value)) {
509  $flatArray[] = [$prefix . $key => $value];
510  } else {
511  $flatArray[] = $flattenRecursive($value, $prefix . $key . '.');
512  }
513  }
514 
515  return array_merge(...$flatArray);
516  };
517 
518  return $flattenRecursive($array);
519  }
520 
541  public static function unflatten(array $input, string $delimiter = '.'): array
542  {
543  $output = [];
544  foreach ($input as $key => $value) {
545  $parts = ‪StringUtility::explodeEscaped($delimiter, $key);
546  $nested = &$output;
547  while (count($parts) > 1) {
548  $nested = &$nested[array_shift($parts)];
549  if (!is_array($nested)) {
550  $nested = [];
551  }
552  }
553  $nested[array_shift($parts)] = $value;
554  }
555  return $output;
556  }
557 
593  public static function intersectRecursive(array $source, array $mask = []): array
594  {
595  $intersection = [];
596  foreach ($source as $key => $_) {
597  if (!array_key_exists($key, $mask)) {
598  continue;
599  }
600  if (is_array($source[$key]) && is_array($mask[$key])) {
601  $value = self::intersectRecursive($source[$key], $mask[$key]);
602  if (!empty($value)) {
603  $intersection[$key] = $value;
604  }
605  } else {
606  $intersection[$key] = $source[$key];
607  }
608  }
609  return $intersection;
610  }
611 
636  public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], int $level = 0): array
637  {
638  $level++;
639  $allKeysAreNumeric = true;
640  foreach ($array as $key => $_) {
641  if (is_int($key) === false) {
642  $allKeysAreNumeric = false;
643  break;
644  }
645  }
646  $renumberedArray = $array;
647  if ($allKeysAreNumeric === true) {
648  $renumberedArray = array_values($array);
649  }
650  foreach ($renumberedArray as $key => $value) {
651  if (is_array($value)) {
652  $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
653  }
654  }
655  return $renumberedArray;
656  }
657 
677  public static function mergeRecursiveWithOverrule(array &$original, array $overrule, bool $addKeys = true, bool $includeEmptyValues = true, bool $enableUnsetFeature = true): void
678  {
679  foreach ($overrule as $key => $_) {
680  if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
681  unset($original[$key]);
682  continue;
683  }
684  if (isset($original[$key]) && is_array($original[$key])) {
685  if (is_array($overrule[$key])) {
686  self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
687  }
688  } elseif (
689  ($addKeys || isset($original[$key])) &&
690  ($includeEmptyValues || $overrule[$key])
691  ) {
692  $original[$key] = $overrule[$key];
693  }
694  }
695  // This line is kept for backward compatibility reasons.
696  reset($original);
697  }
698 
706  public static function removeArrayEntryByValue(array $array, string $cmpValue): array
707  {
708  foreach ($array as $k => $v) {
709  if (is_array($v)) {
710  $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
711  } elseif ((string)$v === $cmpValue) {
712  unset($array[$k]);
713  }
714  }
715  return $array;
716  }
717 
747  public static function keepItemsInArray(array $array, array|string|null $keepItems, ?callable $getValueFunc = null): array
748  {
749  if (empty($array)) {
750  return $array;
751  }
752 
753  // Convert strings to arrays:
754  if (is_string($keepItems)) {
755  $keepItems = ‪GeneralUtility::trimExplode(',', $keepItems);
756  }
757 
758  if (empty($keepItems)) {
759  return $array;
760  }
761 
762  // Check if valueFunc can be executed:
763  if (!is_callable($getValueFunc)) {
764  $getValueFunc = null;
765  }
766  // Do the filtering:
767  if (is_array($keepItems)) {
768  $keepItems = array_flip($keepItems);
769  foreach ($array as $key => $value) {
770  // Get the value to compare by using the callback function:
771  $keepValue = isset($getValueFunc) ? $getValueFunc($value) : $value;
772  if (!isset($keepItems[$keepValue])) {
773  unset($array[$key]);
774  }
775  }
776  }
777 
778  return $array;
779  }
780 
787  public static function remapArrayKeys(array &$array, array $mappingTable): void
788  {
789  foreach ($mappingTable as $old => $new) {
790  if ($new && isset($array[$old])) {
791  $array[$new] = $array[$old];
792  unset($array[$old]);
793  }
794  }
795  }
796 
805  public static function arrayDiffKeyRecursive(array $array1, array $array2): array
806  {
807  $differenceArray = [];
808  foreach ($array1 as $key => $value) {
809  if (!array_key_exists($key, $array2)) {
810  $differenceArray[$key] = $value;
811  } elseif (is_array($value)) {
812  if (is_array($array2[$key])) {
813  $recursiveResult = self::arrayDiffKeyRecursive($value, $array2[$key]);
814  if (!empty($recursiveResult)) {
815  $differenceArray[$key] = $recursiveResult;
816  }
817  }
818  }
819  }
820  return $differenceArray;
821  }
822 
831  public static function arrayDiffAssocRecursive(array $array1, array $array2): array
832  {
833  $differenceArray = [];
834  foreach ($array1 as $key => $value) {
835  if (!array_key_exists($key, $array2) || (!is_array($value) && $value !== $array2[$key])) {
836  $differenceArray[$key] = $value;
837  } elseif (is_array($value)) {
838  if (is_array($array2[$key])) {
839  $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key]);
840  if (!empty($recursiveResult)) {
841  $differenceArray[$key] = $recursiveResult;
842  }
843  }
844  }
845  }
846  return $differenceArray;
847  }
848 
855  public static function naturalKeySortRecursive(array &$array): bool
856  {
857  uksort($array, 'strnatcasecmp');
858  foreach ($array as &$value) {
859  if (is_array($value)) {
860  self::naturalKeySortRecursive($value);
861  }
862  }
863 
864  return true;
865  }
866 
876  public static function filterAndSortByNumericKeys(array $setupArr, bool $acceptAnyKeys = false): array
877  {
878  $filteredKeys = [];
879  $keys = array_keys($setupArr);
880  foreach ($keys as $key) {
881  if ($acceptAnyKeys || ‪MathUtility::canBeInterpretedAsInteger($key)) {
882  $filteredKeys[] = (int)$key;
883  }
884  }
885  $filteredKeys = array_unique($filteredKeys);
886  sort($filteredKeys);
887  return $filteredKeys;
888  }
889 
893  public static function sortArrayWithIntegerKeys(array $array): array
894  {
895  // Can't use array_is_list() because an all-integers but non-sequential
896  // array is not a list, but can still be numerically sorted.
897  if (count(array_filter(array_keys($array), is_string(...))) === 0) {
898  ksort($array);
899  }
900  return $array;
901  }
902 
907  public static function sortArrayWithIntegerKeysRecursive(array $array): array
908  {
909  $array = static::sortArrayWithIntegerKeys($array);
910  foreach ($array as $key => $value) {
911  if (is_array($value) && !empty($value)) {
912  $array[$key] = self::sortArrayWithIntegerKeysRecursive($value);
913  }
914  }
915  return $array;
916  }
917 
921  public static function stripTagsFromValuesRecursive(array $array): array
922  {
923  $result = $array;
924  foreach ($result as $key => $value) {
925  if (is_array($value)) {
926  $result[$key] = self::stripTagsFromValuesRecursive($value);
927  } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
928  $result[$key] = strip_tags((string)$value);
929  }
930  }
931  return $result;
932  }
933 
947  public static function filterRecursive(array $array, ?callable $callback = null, int $mode = 0): array
948  {
949  $callback ??= static fn($value) => (bool)$value;
950 
951  foreach ($array as $key => $value) {
952  if (is_array($value)) {
953  $array[$key] = self::filterRecursive($value, $callback, $mode);
954  }
955  }
956  return array_filter($array, $callback, $mode);
957  }
958 
966  public static function ‪isAssociative(array $array): bool
967  {
968  return !array_is_list($array);
969  }
970 
977  public static function ‪replaceAndAppendScalarValuesRecursive(array $array1, array $array2): array
978  {
979  // Simple lists get merged / added up
980  if (array_is_list($array1)) {
981  return array_merge($array1, $array2);
982  }
983  foreach ($array1 as $k => $v) {
984  // The key also exists in second array, if it is a simple value
985  // then $array2 will override the value, where an array is calling
986  // replaceAndAppendScalarValuesRecursive() recursively.
987  if (isset($array2[$k])) {
988  if (is_array($v) && is_array($array2[$k])) {
989  $array1[$k] = ‪self::replaceAndAppendScalarValuesRecursive($v, $array2[$k]);
990  } else {
991  $array1[$k] = $array2[$k];
992  }
993  unset($array2[$k]);
994  }
995  }
996  // If there are properties in the second array left, they are added up
997  if (!empty($array2)) {
998  foreach ($array2 as $k => $v) {
999  $array1[$k] = $v;
1000  }
1001  }
1002 
1003  return $array1;
1004  }
1005 }
‪TYPO3\CMS\Core\Utility\ArrayUtility\isAssociative
‪static bool isAssociative(array $array)
Definition: ArrayUtility.php:966
‪TYPO3\CMS\Core\Utility\ArrayUtility\flattenPlain
‪static flattenPlain(array $array)
Definition: ArrayUtility.php:502
‪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:977
‪TYPO3\CMS\Core\Utility\ArrayUtility\arrayExport
‪static string arrayExport(array $array=[], int $level=0)
Definition: ArrayUtility.php:392
‪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:475
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Utility\ArrayUtility\removeNullValuesRecursive
‪static removeNullValuesRecursive(array $array)
Definition: ArrayUtility.php:222
‪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:822