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