$value) { if (is_array($value)) { $result[$key] = self::convertBooleanStringsToBooleanRecursive($value); } else { if ($value === 'true') { $result[$key] = true; } elseif ($value === 'false') { $result[$key] = false; } } } return $result; } /** * Reduce an array by a search value and keep the array structure. * * Comparison is type strict: * - For a given needle of type string, integer, array or boolean, * value and value type must match to occur in result array * - For a given object, an object within the array must be a reference to * the same object to match (not just different instance of same class) * * Example: * - Needle: 'findMe' * - Given array: * array( * 'foo' => 'noMatch', * 'bar' => 'findMe', * 'foobar => array( * 'foo' => 'findMe', * ), * ); * - Result: * array( * 'bar' => 'findMe', * 'foobar' => array( * 'foo' => findMe', * ), * ); * * See the unit tests for more examples and expected behaviour * * @param mixed $needle The value to search for * @param array $haystack The array in which to search * @return array $haystack array reduced matching $needle values */ public static function filterByValueRecursive(mixed $needle = '', array $haystack = []): array { $resultArray = []; // Define a lambda function to be applied to all members of this array dimension // Call recursive if current value is of type array // Write to $resultArray (by reference!) if types and value match $callback = static function (&$value, $key) use ($needle, &$resultArray) { if ($value === $needle) { $resultArray[$key] = $value; } elseif (is_array($value)) { $subArrayMatches = static::filterByValueRecursive($needle, $value); if (!empty($subArrayMatches)) { $resultArray[$key] = $subArrayMatches; } } }; // array_walk() is not affected by the internal pointers, no need to reset array_walk($haystack, $callback); // Pointers to result array are reset internally return $resultArray; } /** * Checks if a given path exists in array * * Example: * - array: * array( * 'foo' => array( * 'bar' = 'test', * ) * ); * - path: 'foo/bar' * - return: TRUE * * @param array $array Given array * @param array|string $path Path to test within the array * @param string $delimiter Delimiter for path, default / * @return bool TRUE if path exists in array */ public static function isValidPath(array $array, array|string $path, string $delimiter = '/'): bool { $isValid = true; try { static::getValueByPath($array, $path, $delimiter); } catch (MissingArrayPathException) { $isValid = false; } return $isValid; } /** * Returns a value by given path * * Example * - array: * array( * 'foo' => array( * 'bar' => array( * 'baz' => 42 * ) * ) * ); * - path: foo/bar/baz * - return: 42 * * If a path segments contains a delimiter character, the path segment * must be enclosed by " (double quote), see unit tests for details * * @param array $array Input array * @param array|string $path Path within the array * @param string $delimiter Defined path delimiter, default / * @throws \RuntimeException if the path is empty * @throws MissingArrayPathException if a configured path segment does not exist in the array */ public static function getValueByPath(array $array, array|string $path, string $delimiter = '/'): mixed { // Upcast a string to an array if necessary if (is_string($path)) { if ($path === '') { // Programming error has to be sanitized before calling the method -> global exception throw new \RuntimeException('Path must not be empty', 1341397767); } $path = str_getcsv($path, $delimiter, '"', '\\'); } // Loop through each part and extract its value $value = $array; foreach ($path as $segment) { if (is_array($value) && array_key_exists($segment, $value)) { // Replace current value with child $value = $value[$segment]; } else { // Throw specific exception if there is no such path throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1341397869); } } return $value; } /** * Reindex keys from the current nesting level if all keys within * the current nesting level are integers. */ public static function reIndexNumericArrayKeysRecursive(array $array): array { // Can't use array_is_list() because an all-integers but non-sequential // array is not a list, but should be reindexed. if (count(array_filter(array_keys($array), is_string(...))) === 0) { $array = array_values($array); } foreach ($array as $key => $value) { if (is_array($value) && !empty($value)) { $array[$key] = self::reIndexNumericArrayKeysRecursive($value); } } return $array; } /** * Recursively remove keys if their value are NULL. */ public static function removeNullValuesRecursive(array $array): array { $result = $array; foreach ($result as $key => $value) { if (is_array($value)) { $result[$key] = self::removeNullValuesRecursive($value); } elseif ($value === null) { unset($result[$key]); } } return $result; } /** * Modifies or sets a new value in an array by given path * * Example: * - array: * array( * 'foo' => array( * 'bar' => 42, * ), * ); * - path: foo/bar * - value: 23 * - return: * array( * 'foo' => array( * 'bar' => 23, * ), * ); * * @param array $array Input array to manipulate * @param string|array|\ArrayAccess $path Path in array to search for * @param mixed $value Value to set at path location in array * @param string $delimiter Path delimiter * @return array Modified array * @throws \RuntimeException */ public static function setValueByPath(array $array, string|array|\ArrayAccess $path, mixed $value, string $delimiter = '/'): array { if (is_string($path)) { if ($path === '') { throw new \RuntimeException('Path must not be empty', 1341406194); } // Extract parts of the path $path = str_getcsv($path, $delimiter, '"', '\\'); } // Point to the root of the array $pointer = &$array; // Find path in given array foreach ($path as $segment) { // Fail if the part is empty if ($segment === '') { throw new \RuntimeException('Invalid path segment specified', 1341406846); } // Create cell if it doesn't exist if (is_array($pointer) && !array_key_exists($segment, $pointer)) { $pointer[$segment] = []; } // Make it array if it was something else before if (!is_array($pointer)) { $pointer = []; } // Set pointer to new cell $pointer = &$pointer[$segment]; } // Set value of target cell $pointer = $value; return $array; } /** * Remove a sub part from an array specified by path * * @param array $array Input array to manipulate * @param string $path Path to remove from array * @param string $delimiter Path delimiter * @return array Modified array * @throws \RuntimeException */ public static function removeByPath(array $array, string $path, string $delimiter = '/'): array { if ($path === '') { throw new \RuntimeException('Path must not be empty', 1371757718); } // Extract parts of the path $pathSegments = str_getcsv($path, $delimiter, '"', '\\'); $pathDepth = count($pathSegments); $currentDepth = 0; $pointer = &$array; // Find path in given array foreach ($pathSegments as $segment) { $currentDepth++; // Fail if the part is empty if ($segment === '') { throw new \RuntimeException('Invalid path segment specified', 1371757720); } if (!array_key_exists($segment, $pointer)) { throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $pathSegments) . ' does not exist in array', 1371758436); } if ($currentDepth === $pathDepth) { unset($pointer[$segment]); } else { $pointer = &$pointer[$segment]; } } return $array; } /** * Sorts an array recursively by key * * @param array $array Array to sort recursively by key * @return array Sorted array */ public static function sortByKeyRecursive(array $array): array { ksort($array); foreach ($array as $key => $value) { if (is_array($value) && !empty($value)) { $array[$key] = self::sortByKeyRecursive($value); } } return $array; } /** * Sort an array of arrays by a given key using uasort * * @param array $arrays Array of arrays to sort * @param string $key Key to sort after * @param bool $ascending Set to TRUE for ascending order, FALSE for descending order * @return array Array of sorted arrays * @throws \RuntimeException */ public static function sortArraysByKey(array $arrays, string $key, bool $ascending = true): array { if (empty($arrays)) { return $arrays; } uasort($arrays, static function (array $a, array $b) use ($key, $ascending) { if (!isset($a[$key], $b[$key])) { throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309); } if (!is_scalar($a[$key])) { throw new \RuntimeException(sprintf('The specified sorting key "%s" is not a scalar value, given "%s".', $key, gettype($a[$key])), 1373727310); } if (!is_scalar($b[$key])) { throw new \RuntimeException(sprintf('The specified sorting key "%s" is not a scalar value, given "%s".', $key, gettype($b[$key])), 1373727311); } return $ascending ? strcasecmp((string)$a[$key], (string)$b[$key]) : strcasecmp((string)$b[$key], (string)$a[$key]); }); return $arrays; } /** * Exports an array as string. * Similar to var_export(), but representation follows the PSR-2 and TYPO3 core CGL. * * See unit tests for detailed examples * * @param array $array Array to export * @param int $level Internal level used for recursion, do *not* set from outside! * @return string String representation of array * @throws \RuntimeException */ public static function arrayExport(array $array = [], int $level = 0): string { $lines = "[\n"; $level++; $writeKeyIndex = false; $expectedKeyIndex = 0; foreach ($array as $key => $value) { if ($key === $expectedKeyIndex) { $expectedKeyIndex++; } else { // Found a non-integer or non-consecutive key, so we can break here $writeKeyIndex = true; break; } } foreach ($array as $key => $value) { // Indention $lines .= str_repeat(' ', $level); if ($writeKeyIndex) { // Numeric / string keys $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => '; } if (is_array($value)) { if (!empty($value)) { $lines .= self::arrayExport($value, $level); } else { $lines .= "[],\n"; } } elseif (is_int($value) || is_float($value)) { $lines .= $value . ",\n"; } elseif ($value === null) { $lines .= "null,\n"; } elseif (is_bool($value)) { $lines .= $value ? 'true' : 'false'; $lines .= ",\n"; } elseif (is_string($value)) { // Quote \ to \\ // Quote ' to \' $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value); $lines .= '\'' . $stringContent . "',\n"; } else { throw new \RuntimeException('Objects are not supported', 1342294987); } } $lines .= str_repeat(' ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ",\n"); return $lines; } /** * Converts a multidimensional array to a flat representation. * @todo: The current implementation isn't a generic array flatten method, but tailored for TypoScript flattening * @todo: It should be deprecated and removed and the required specialities should be put under the domain of TypoScript parsing * * See unit tests for more details * * Example: * - array: * array( * 'first.' => array( * 'second' => 1 * ) * ) * - result: * array( * 'first.second' => 1 * ) * * Example: * - array: * array( * 'first' => array( * 'second' => 1 * ) * ) * - result: * array( * 'first.second' => 1 * ) * * @param array $array The (relative) array to be converted * @param string $prefix The (relative) prefix to be used (e.g. 'section.') * @param bool $keepDots */ public static function flatten(array $array, string $prefix = '', bool $keepDots = false): array { $flatArray = []; foreach ($array as $key => $value) { if ($keepDots === false) { // Ensure there is no trailing dot: $key = rtrim((string)$key, '.'); } if (!is_array($value)) { $flatArray[$prefix . $key] = $value; } else { $newPrefix = $prefix . $key; if ($keepDots === false) { $newPrefix = $prefix . $key . '.'; } $flatArray = array_merge($flatArray, self::flatten($value, $newPrefix, $keepDots)); } } return $flatArray; } /** * Just like flatten, but not tailored for TypoScript but for plain simple arrays * It is internal for now, as it needs to be decided how to deprecate/ rename flatten * * @internal */ public static function flattenPlain(array $array): array { $flattenRecursive = static function (array $array, string $prefix = '') use (&$flattenRecursive) { $flatArray = []; foreach ($array as $key => $value) { $key = addcslashes((string)$key, '.'); if (!is_array($value)) { $flatArray[] = [$prefix . $key => $value]; } else { $flatArray[] = $flattenRecursive($value, $prefix . $key . '.'); } } return array_merge(...$flatArray); }; return $flattenRecursive($array); } /** * Converts a flat representation of an array to a multidimensional array. * * Example: * - array: * array( * 'first.second' => 1 * ) * * - result: * array( * 'first.' => array( * 'second' => 1 * ) * ) * * @param array $input * @param string $delimiter * @return array */ public static function unflatten(array $input, string $delimiter = '.'): array { $output = []; foreach ($input as $key => $value) { $parts = StringUtility::explodeEscaped($delimiter, $key); $nested = &$output; while (count($parts) > 1) { $nested = &$nested[array_shift($parts)]; if (!is_array($nested)) { $nested = []; } } $nested[array_shift($parts)] = $value; } return $output; } /** * Determine the intersections between two arrays, recursively comparing keys * A complete sub array of $source will be preserved, if the key exists in $mask. * * See unit tests for more examples and edge cases. * * Example: * - source: * array( * 'key1' => 'bar', * 'key2' => array( * 'subkey1' => 'sub1', * 'subkey2' => 'sub2', * ), * 'key3' => 'baz', * ) * - mask: * array( * 'key1' => NULL, * 'key2' => array( * 'subkey1' => exists', * ), * ) * - return: * array( * 'key1' => 'bar', * 'key2' => array( * 'subkey1' => 'sub1', * ), * ) * * @param array $source Source array * @param array $mask Array that has the keys which should be kept in the source array * @return array Keys which are present in both arrays with values of the source array */ public static function intersectRecursive(array $source, array $mask = []): array { $intersection = []; foreach ($source as $key => $_) { if (!array_key_exists($key, $mask)) { continue; } if (is_array($source[$key]) && is_array($mask[$key])) { $value = self::intersectRecursive($source[$key], $mask[$key]); if (!empty($value)) { $intersection[$key] = $value; } } else { $intersection[$key] = $source[$key]; } } return $intersection; } /** * Renumber the keys of an array to avoid leaps if keys are all numeric. * * Is called recursively for nested arrays. * * Example: * * Given * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three') * as input, it will return * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three') * * Will treat keys string representations of number (ie. '1') equal to the * numeric value (ie. 1). * * Example: * Given * array('0' => 'Zero', '1' => 'One' ) * it will return * array(0 => 'Zero', 1 => 'One') * * @param array $array Input array * @param int $level Internal level used for recursion, do *not* set from outside! */ public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], int $level = 0): array { $level++; $allKeysAreNumeric = true; foreach ($array as $key => $_) { if (is_int($key) === false) { $allKeysAreNumeric = false; break; } } $renumberedArray = $array; if ($allKeysAreNumeric === true) { $renumberedArray = array_values($array); } foreach ($renumberedArray as $key => $value) { if (is_array($value)) { $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level); } } return $renumberedArray; } /** * Merges two arrays recursively and "binary safe" (integer keys are * overridden as well), overruling similar values in the original array * with the values of the overrule array. * In case of identical keys, ie. keeping the values of the overrule array. * * This method takes the original array by reference for speed optimization with large arrays * * The differences to the existing PHP function array_merge_recursive() are: * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature) * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues) * * Elements or the original array get overwritten if the same key is present in the overrule array. * * @param array $original Original array. It will be *modified* by this method and contains the result afterwards! * @param array $overrule Overrule array, overruling the original array * @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. * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero. * @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. */ public static function mergeRecursiveWithOverrule(array &$original, array $overrule, bool $addKeys = true, bool $includeEmptyValues = true, bool $enableUnsetFeature = true): void { foreach ($overrule as $key => $_) { if ($enableUnsetFeature && $overrule[$key] === '__UNSET') { unset($original[$key]); continue; } if (isset($original[$key]) && is_array($original[$key])) { if (is_array($overrule[$key])) { self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature); } } elseif ( ($addKeys || isset($original[$key])) && ($includeEmptyValues || $overrule[$key]) ) { $original[$key] = $overrule[$key]; } } // This line is kept for backward compatibility reasons. reset($original); } /** * Removes the value $cmpValue from the $array if found there. Returns the modified array * * @param array $array Array containing the values * @param string $cmpValue Value to search for and if found remove array entry where found. * @return array Output array with entries removed if search string is found */ public static function removeArrayEntryByValue(array $array, string $cmpValue): array { foreach ($array as $k => $v) { if (is_array($v)) { $array[$k] = self::removeArrayEntryByValue($v, $cmpValue); } elseif ((string)$v === $cmpValue) { unset($array[$k]); } } return $array; } /** * Filters an array to reduce its elements to match the condition. * The values in $keepItems can be optionally evaluated by a custom callback function. * * Example (arguments used to call this function): * * ``` * $array = array( * array('aa' => array('first', 'second'), * array('bb' => array('third', 'fourth'), * array('cc' => array('fifth', 'sixth'), * ); * $keepItems = array('third'); * $getValueFunc = function($value) { return $value[0]; } * ``` * * Returns: * * ``` * array( * array('bb' => array('third', 'fourth'), * ) * ``` * * @param array $array $array The initial array to be filtered/reduced * @param array|string|null $keepItems The items which are allowed/kept in the array - accepts array or csv string * @param callable|null $getValueFunc (optional) Callback function used to get the value to keep * @return array The filtered/reduced array with the kept items */ public static function keepItemsInArray(array $array, array|string|null $keepItems, ?callable $getValueFunc = null): array { if (empty($array)) { return $array; } // Convert strings to arrays: if (is_string($keepItems)) { $keepItems = GeneralUtility::trimExplode(',', $keepItems); } if (empty($keepItems)) { return $array; } // Check if valueFunc can be executed: if (!is_callable($getValueFunc)) { $getValueFunc = null; } // Do the filtering: $keepItems = array_flip($keepItems); foreach ($array as $key => $value) { // Get the value to compare by using the callback function: $keepValue = isset($getValueFunc) ? $getValueFunc($value) : $value; if (!isset($keepItems[$keepValue])) { unset($array[$key]); } } return $array; } /** * Rename Array keys with a given mapping table * * @param array $array Array by reference which should be remapped * @param array $mappingTable Array with remap information, array/$oldKey => $newKey) */ public static function remapArrayKeys(array &$array, array $mappingTable): void { foreach ($mappingTable as $old => $new) { if ($new && isset($array[$old])) { $array[$new] = $array[$old]; unset($array[$old]); } } } /** * Filters keys off from first array that also exist in second array. Comparison is done by keys. * This method is a recursive version of php array_diff_key() * * @param array $array1 Source array * @param array $array2 Reduce source array by this array * @return array Source array reduced by keys also present in second array */ public static function arrayDiffKeyRecursive(array $array1, array $array2): array { $differenceArray = []; foreach ($array1 as $key => $value) { if (!array_key_exists($key, $array2)) { $differenceArray[$key] = $value; } elseif (is_array($value)) { if (is_array($array2[$key])) { $recursiveResult = self::arrayDiffKeyRecursive($value, $array2[$key]); if (!empty($recursiveResult)) { $differenceArray[$key] = $recursiveResult; } } } } return $differenceArray; } /** * Filters values off from first array that also exist in second array. Comparison is done by keys. * This method is a recursive version of php array_diff_assoc() * * @param array $array1 Source array * @param array $array2 Reduce source array by this array * @return array Source array reduced by values also present in second array, indexed by key */ public static function arrayDiffAssocRecursive(array $array1, array $array2): array { $differenceArray = []; foreach ($array1 as $key => $value) { if (!array_key_exists($key, $array2) || (!is_array($value) && $value !== $array2[$key])) { $differenceArray[$key] = $value; } elseif (is_array($value)) { if (is_array($array2[$key])) { $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key]); if (!empty($recursiveResult)) { $differenceArray[$key] = $recursiveResult; } } } } return $differenceArray; } /** * Sorts an array by key recursive - uses natural sort order (aAbB-zZ) * * @param array $array array to be sorted recursively, passed by reference * @return bool always TRUE */ public static function naturalKeySortRecursive(array &$array): bool { uksort($array, 'strnatcasecmp'); foreach ($array as &$value) { if (is_array($value)) { self::naturalKeySortRecursive($value); } } return true; } /** * 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. * * @param array $setupArr TypoScript array with numerical array in * @param bool $acceptAnyKeys If set, then a value is not required - the properties alone will be enough. * @return array An array with all integer properties listed in numeric order. * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGet() * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder */ public static function filterAndSortByNumericKeys(array $setupArr, bool $acceptAnyKeys = false): array { $filteredKeys = []; $keys = array_keys($setupArr); foreach ($keys as $key) { if ($acceptAnyKeys || MathUtility::canBeInterpretedAsInteger($key)) { $filteredKeys[] = (int)$key; } } $filteredKeys = array_unique($filteredKeys); sort($filteredKeys); return $filteredKeys; } /** * If the array contains numerical keys only, sort it in ascending order */ public static function sortArrayWithIntegerKeys(array $array): array { // Can't use array_is_list() because an all-integers but non-sequential // array is not a list, but can still be numerically sorted. if (count(array_filter(array_keys($array), is_string(...))) === 0) { ksort($array); } return $array; } /** * Sort keys from the current nesting level if all keys within the * current nesting level are integers. */ public static function sortArrayWithIntegerKeysRecursive(array $array): array { $array = static::sortArrayWithIntegerKeys($array); foreach ($array as $key => $value) { if (is_array($value) && !empty($value)) { $array[$key] = self::sortArrayWithIntegerKeysRecursive($value); } } return $array; } /** * Recursively translate values. */ public static function stripTagsFromValuesRecursive(array $array): array { $result = $array; foreach ($result as $key => $value) { if (is_array($value)) { $result[$key] = self::stripTagsFromValuesRecursive($value); } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) { $result[$key] = strip_tags((string)$value); } } return $result; } /** * Recursively filter an array * * Example: * filterRecursive( * ['a' => ['b' => null]], * static fn ($item) => $item !== null, * ARRAY_FILTER_USE_BOTH * ) * * @param 0|ARRAY_FILTER_USE_KEY|ARRAY_FILTER_USE_BOTH $mode * @see https://www.php.net/manual/en/function.array-filter.php */ public static function filterRecursive(array $array, ?callable $callback = null, int $mode = 0): array { $callback ??= static fn($value) => (bool)$value; foreach ($array as $key => $value) { if (is_array($value)) { $array[$key] = self::filterRecursive($value, $callback, $mode); } } return array_filter($array, $callback, $mode); } /** * Check whether the array has non-integer keys. If there is at least one string key, $array will be * regarded as an associative array. * * @return bool True in case a string key was found. * @internal */ public static function isAssociative(array $array): bool { return !array_is_list($array); } /** * Same as array_replace_recursive except that when in simple arrays (= YAML lists), the entries are * appended (array_merge). The second array takes precedence in case of equal sub arrays. * * @internal */ public static function replaceAndAppendScalarValuesRecursive(array $array1, array $array2): array { // Simple lists get merged / added up if (array_is_list($array1)) { return array_merge($array1, $array2); } foreach ($array1 as $k => $v) { // The key also exists in second array, if it is a simple value // then $array2 will override the value, where an array is calling // replaceAndAppendScalarValuesRecursive() recursively. if (isset($array2[$k])) { if (is_array($v) && is_array($array2[$k])) { $array1[$k] = self::replaceAndAppendScalarValuesRecursive($v, $array2[$k]); } else { $array1[$k] = $array2[$k]; } unset($array2[$k]); } } // If there are properties in the second array left, they are added up if (!empty($array2)) { foreach ($array2 as $k => $v) { $array1[$k] = $v; } } return $array1; } /** * Determines whether all (nested) array values are scalar values or `null`. */ public static function containsOnlyScalarValues(array $array): bool { foreach ($array as $value) { if (is_array($value)) { if (!self::containsOnlyScalarValues($value)) { return false; } } elseif (!is_scalar($value) && $value !== null) { return false; } } return true; } }