[TASK] Cleanup ArrayUtility::sortArraysByKey()
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / ArrayUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2011-2013 Susanne Moog <typo3@susanne-moog.de>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /**
30 * Class with helper functions for array handling
31 *
32 * @author Susanne Moog <typo3@susanne-moog.de>
33 */
34 class ArrayUtility {
35
36 /**
37 * Reduce an array by a search value and keep the array structure.
38 *
39 * Comparison is type strict:
40 * - For a given needle of type string, integer, array or boolean,
41 * value and value type must match to occur in result array
42 * - For a given object, a object within the array must be a reference to
43 * the same object to match (not just different instance of same class)
44 *
45 * Example:
46 * - Needle: 'findMe'
47 * - Given array:
48 * array(
49 * 'foo' => 'noMatch',
50 * 'bar' => 'findMe',
51 * 'foobar => array(
52 * 'foo' => 'findMe',
53 * ),
54 * );
55 * - Result:
56 * array(
57 * 'bar' => 'findMe',
58 * 'foobar' => array(
59 * 'foo' => findMe',
60 * ),
61 * );
62 *
63 * See the unit tests for more examples and expected behaviour
64 *
65 * @param mixed $needle The value to search for
66 * @param array $haystack The array in which to search
67 * @return array $haystack array reduced matching $needle values
68 */
69 static public function filterByValueRecursive($needle = '', array $haystack = array()) {
70 $resultArray = array();
71 // Define a lambda function to be applied to all members of this array dimension
72 // Call recursive if current value is of type array
73 // Write to $resultArray (by reference!) if types and value match
74 $callback = function (&$value, $key) use($needle, &$resultArray) {
75 if ($value === $needle) {
76 ($resultArray[$key] = $value);
77 } elseif (is_array($value)) {
78 ($subArrayMatches = \TYPO3\CMS\Core\Utility\ArrayUtility::filterByValueRecursive($needle, $value));
79 if (count($subArrayMatches) > 0) {
80 ($resultArray[$key] = $subArrayMatches);
81 }
82 }
83 };
84 // array_walk() is not affected by the internal pointers, no need to reset
85 array_walk($haystack, $callback);
86 // Pointers to result array are reset internally
87 return $resultArray;
88 }
89
90 /**
91 * Checks if a given path exists in array
92 *
93 * Example:
94 * - array:
95 * array(
96 * 'foo' => array(
97 * 'bar' = 'test',
98 * )
99 * );
100 * - path: 'foo/bar'
101 * - return: TRUE
102 *
103 * @param array $array Given array
104 * @param string $path Path to test, 'foo/bar/foobar'
105 * @param string $delimiter Delimeter for path, default /
106 * @return boolean TRUE if path exists in array
107 */
108 static public function isValidPath(array $array, $path, $delimiter = '/') {
109 $isValid = TRUE;
110 try {
111 // Use late static binding to enable mocking of this call in unit tests
112 static::getValueByPath($array, $path, $delimiter);
113 } catch (\RuntimeException $e) {
114 $isValid = FALSE;
115 }
116 return $isValid;
117 }
118
119 /**
120 * Returns a value by given path
121 *
122 * Example
123 * - array:
124 * array(
125 * 'foo' => array(
126 * 'bar' => array(
127 * 'baz' => 42
128 * )
129 * )
130 * );
131 * - path: foo/bar/baz
132 * - return: 42
133 *
134 * If a path segments contains a delimiter character, the path segment
135 * must be enclosed by " (double quote), see unit tests for details
136 *
137 * @param array $array Input array
138 * @param string $path Path within the array
139 * @param string $delimiter Defined path delimiter, default /
140 * @return mixed
141 * @throws \RuntimeException
142 */
143 static public function getValueByPath(array $array, $path, $delimiter = '/') {
144 if (empty($path)) {
145 throw new \RuntimeException('Path must not be empty', 1341397767);
146 }
147 // Extract parts of the path
148 $path = str_getcsv($path, $delimiter);
149 // Loop through each part and extract its value
150 $value = $array;
151 foreach ($path as $segment) {
152 if (array_key_exists($segment, $value)) {
153 // Replace current value with child
154 $value = $value[$segment];
155 } else {
156 // Fail if key does not exist
157 throw new \RuntimeException('Path does not exist in array', 1341397869);
158 }
159 }
160 return $value;
161 }
162
163 /**
164 * Modifies or sets a new value in an array by given path
165 *
166 * Example:
167 * - array:
168 * array(
169 * 'foo' => array(
170 * 'bar' => 42,
171 * ),
172 * );
173 * - path: foo/bar
174 * - value: 23
175 * - return:
176 * array(
177 * 'foo' => array(
178 * 'bar' => 23,
179 * ),
180 * );
181 *
182 * @param array $array Input array to manipulate
183 * @param string $path Path in array to search for
184 * @param mixed $value Value to set at path location in array
185 * @param string $delimiter Path delimiter
186 * @return array Modified array
187 * @throws \RuntimeException
188 */
189 static public function setValueByPath(array $array, $path, $value, $delimiter = '/') {
190 if (empty($path)) {
191 throw new \RuntimeException('Path must not be empty', 1341406194);
192 }
193 if (!is_string($path)) {
194 throw new \RuntimeException('Path must be a string', 1341406402);
195 }
196 // Extract parts of the path
197 $path = str_getcsv($path, $delimiter);
198 // Point to the root of the array
199 $pointer = &$array;
200 // Find path in given array
201 foreach ($path as $segment) {
202 // Fail if the part is empty
203 if (empty($segment)) {
204 throw new \RuntimeException('Invalid path segment specified', 1341406846);
205 }
206 // Create cell if it doesn't exist
207 if (!array_key_exists($segment, $pointer)) {
208 $pointer[$segment] = array();
209 }
210 // Set pointer to new cell
211 $pointer = &$pointer[$segment];
212 }
213 // Set value of target cell
214 $pointer = $value;
215 return $array;
216 }
217
218 /**
219 * Remove a sub part from an array specified by path
220 *
221 * @param array $array Input array to manipulate
222 * @param string $path Path to remove from array
223 * @param string $delimiter Path delimiter
224 * @return array Modified array
225 * @throws \RuntimeException
226 */
227 static public function removeByPath(array $array, $path, $delimiter = '/') {
228 if (empty($path)) {
229 throw new \RuntimeException('Path must not be empty', 1371757718);
230 }
231 if (!is_string($path)) {
232 throw new \RuntimeException('Path must be a string', 1371757719);
233 }
234 // Extract parts of the path
235 $path = str_getcsv($path, $delimiter);
236 $pathDepth = count($path);
237 $currentDepth = 0;
238 $pointer = &$array;
239 // Find path in given array
240 foreach ($path as $segment) {
241 $currentDepth++;
242 // Fail if the part is empty
243 if (empty($segment)) {
244 throw new \RuntimeException('Invalid path segment specified', 1371757720);
245 }
246 if (!array_key_exists($segment, $pointer)) {
247 throw new \RuntimeException('Path segment ' . $segment . ' does not exist in array', 1371758436);
248 }
249 if ($currentDepth === $pathDepth) {
250 unset($pointer[$segment]);
251 } else {
252 $pointer = &$pointer[$segment];
253 }
254 }
255 return $array;
256 }
257
258 /**
259 * Sorts an array recursively by key
260 *
261 * @param $array Array to sort recursively by key
262 * @return array Sorted array
263 */
264 static public function sortByKeyRecursive(array $array) {
265 ksort($array);
266 foreach ($array as $key => $value) {
267 if (is_array($value) && !empty($value)) {
268 $array[$key] = self::sortByKeyRecursive($value);
269 }
270 }
271 return $array;
272 }
273
274 /**
275 * Sort an array of arrays by a given key using uasort
276 *
277 * @param array $arrays Array of arrays to sort
278 * @param string $key Key to sort after
279 * @param bool $ascending Set to TRUE for ascending order, FALSE for descending order
280 * @return array Array of sorted arrays
281 * @throws \RuntimeException
282 */
283 static public function sortArraysByKey(array $arrays, $key, $ascending = TRUE) {
284 if (empty($arrays)) {
285 return $arrays;
286 }
287 $sortResult = uasort($arrays, function (array $a, array $b) use ($key, $ascending) {
288 if (!isset($a[$key]) || !isset($b[$key])) {
289 throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
290 }
291 return ($ascending) ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
292 });
293 if (!$sortResult) {
294 throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
295 }
296 return $arrays;
297 }
298
299 /**
300 * Exports an array as string.
301 * Similar to var_export(), but representation follows the TYPO3 core CGL.
302 *
303 * See unit tests for detailed examples
304 *
305 * @param array $array Array to export
306 * @param integer $level Internal level used for recursion, do *not* set from outside!
307 * @return string String representation of array
308 * @throws \RuntimeException
309 */
310 static public function arrayExport(array $array = array(), $level = 0) {
311 $lines = 'array(' . LF;
312 $level++;
313 $writeKeyIndex = FALSE;
314 $expectedKeyIndex = 0;
315 foreach ($array as $key => $value) {
316 if ($key === $expectedKeyIndex) {
317 $expectedKeyIndex++;
318 } else {
319 // Found a non integer or non consecutive key, so we can break here
320 $writeKeyIndex = TRUE;
321 break;
322 }
323 }
324 foreach ($array as $key => $value) {
325 // Indention
326 $lines .= str_repeat(TAB, $level);
327 if ($writeKeyIndex) {
328 // Numeric / string keys
329 $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
330 }
331 if (is_array($value)) {
332 if (count($value) > 0) {
333 $lines .= self::arrayExport($value, $level);
334 } else {
335 $lines .= 'array(),' . LF;
336 }
337 } elseif (is_int($value) || is_float($value)) {
338 $lines .= $value . ',' . LF;
339 } elseif (is_null($value)) {
340 $lines .= 'NULL' . ',' . LF;
341 } elseif (is_bool($value)) {
342 $lines .= $value ? 'TRUE' : 'FALSE';
343 $lines .= ',' . LF;
344 } elseif (is_string($value)) {
345 // Quote \ to \\
346 $stringContent = str_replace('\\', '\\\\', $value);
347 // Quote ' to \'
348 $stringContent = str_replace('\'', '\\\'', $stringContent);
349 $lines .= '\'' . $stringContent . '\'' . ',' . LF;
350 } else {
351 throw new \RuntimeException('Objects are not supported', 1342294987);
352 }
353 }
354 $lines .= str_repeat(TAB, ($level - 1)) . ')' . ($level - 1 == 0 ? '' : ',' . LF);
355 return $lines;
356 }
357
358 /**
359 * Converts a multidimensional array to a flat representation.
360 *
361 * See unit tests for more details
362 *
363 * Example:
364 * - array:
365 * array(
366 * 'first.' => array(
367 * 'second' => 1
368 * )
369 * )
370 * - result:
371 * array(
372 * 'first.second' => 1
373 * )
374 *
375 * Example:
376 * - array:
377 * array(
378 * 'first' => array(
379 * 'second' => 1
380 * )
381 * )
382 * - result:
383 * array(
384 * 'first.second' => 1
385 * )
386 *
387 * @param array $array The (relative) array to be converted
388 * @param string $prefix The (relative) prefix to be used (e.g. 'section.')
389 * @return array
390 */
391 static public function flatten(array $array, $prefix = '') {
392 $flatArray = array();
393 foreach ($array as $key => $value) {
394 // Ensure there is no trailling dot:
395 $key = rtrim($key, '.');
396 if (!is_array($value)) {
397 $flatArray[$prefix . $key] = $value;
398 } else {
399 $flatArray = array_merge($flatArray, self::flatten($value, $prefix . $key . '.'));
400 }
401 }
402 return $flatArray;
403 }
404
405 /**
406 * Determine the intersections between two arrays, recursively comparing keys
407 * A complete sub array of $source will be preserved, if the key exists in $mask.
408 *
409 * See unit tests for more examples and edge cases.
410 *
411 * Example:
412 * - source:
413 * array(
414 * 'key1' => 'bar',
415 * 'key2' => array(
416 * 'subkey1' => 'sub1',
417 * 'subkey2' => 'sub2',
418 * ),
419 * 'key3' => 'baz',
420 * )
421 * - mask:
422 * array(
423 * 'key1' => NULL,
424 * 'key2' => array(
425 * 'subkey1' => exists',
426 * ),
427 * )
428 * - return:
429 * array(
430 * 'key1' => 'bar',
431 * 'key2' => array(
432 * 'subkey1' => 'sub1',
433 * ),
434 * )
435 *
436 * @param array $source Source array
437 * @param array $mask Array that has the keys which should be kept in the source array
438 * @return array Keys which are present in both arrays with values of the source array
439 */
440 public static function intersectRecursive(array $source, array $mask = array()) {
441 $intersection = array();
442 $sourceArrayKeys = array_keys($source);
443 foreach ($sourceArrayKeys as $key) {
444 if (!array_key_exists($key, $mask)) {
445 continue;
446 }
447 if (is_array($source[$key]) && is_array($mask[$key])) {
448 $value = self::intersectRecursive($source[$key], $mask[$key]);
449 if (!empty($value)) {
450 $intersection[$key] = $value;
451 }
452 } else {
453 $intersection[$key] = $source[$key];
454 }
455 }
456 return $intersection;
457 }
458
459 /**
460 * Renumber the keys of an array to avoid leaps if keys are all numeric.
461 *
462 * Is called recursively for nested arrays.
463 *
464 * Example:
465 *
466 * Given
467 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three')
468 * as input, it will return
469 * array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three')
470 *
471 * Will treat keys string representations of number (ie. '1') equal to the
472 * numeric value (ie. 1).
473 *
474 * Example:
475 * Given
476 * array('0' => 'Zero', '1' => 'One' )
477 * it will return
478 * array(0 => 'Zero', 1 => 'One')
479 *
480 * @param array $array Input array
481 * @param integer $level Internal level used for recursion, do *not* set from outside!
482 * @return array
483 */
484 static public function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = array(), $level = 0) {
485 $level++;
486 $allKeysAreNumeric = TRUE;
487 foreach (array_keys($array) as $key) {
488 if (is_numeric($key) === FALSE) {
489 $allKeysAreNumeric = FALSE;
490 break;
491 }
492 }
493 $renumberedArray = $array;
494 if ($allKeysAreNumeric === TRUE) {
495 $renumberedArray = array_values($array);
496 }
497 foreach ($renumberedArray as $key => $value) {
498 if (is_array($value)) {
499 $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
500 }
501 }
502 return $renumberedArray;
503 }
504
505
506 /**
507 * Merges two arrays recursively and "binary safe" (integer keys are
508 * overridden as well), overruling similar values in the original array
509 * with the values of the overrule array.
510 * In case of identical keys, ie. keeping the values of the overrule array.
511 *
512 * This method takes the original array by reference for speed optimization with large arrays
513 *
514 * The differences to the existing PHP function array_merge_recursive() are:
515 * * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
516 * * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
517 * * Elements or the original array get overwritten if the same key is present in the overrule array.
518 *
519 * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
520 * @param array $overrule Overrule array, overruling the original array
521 * @param boolean $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.
522 * @param boolean $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
523 * @param boolean $enableUnsetFeature If set, special values "__UNSET" can be used in the overrule array in order to unset array keys in the original array.
524 * @return void
525 */
526 static public function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = TRUE, $includeEmptyValues = TRUE, $enableUnsetFeature = TRUE) {
527 foreach (array_keys($overrule) as $key) {
528 if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
529 unset($original[$key]);
530 continue;
531 }
532 if (isset($original[$key]) && is_array($original[$key])) {
533 if (is_array($overrule[$key])) {
534 self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
535 }
536 } elseif (
537 ($addKeys || isset($original[$key])) &&
538 ($includeEmptyValues || $overrule[$key])
539 ) {
540 $original[$key] = $overrule[$key];
541 }
542 }
543 // This line is kept for backward compatibility reasons.
544 reset($original);
545 }
546 }