Passed
Push — master ( cf162b...29ec4f )
by
unknown
13:30
created

ArrayUtility::arrayDiffAssocRecursive()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 28
rs 8.0555
c 0
b 0
f 0
cc 9
nc 7
nop 3
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Utility;
17
18
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
19
20
/**
21
 * Class with helper functions for array handling
22
 */
23
class ArrayUtility
24
{
25
    /**
26
     * Validates the given $arrayToTest by checking if an element is not in $allowedArrayKeys.
27
     *
28
     * @param array $arrayToTest
29
     * @param array $allowedArrayKeys
30
     * @throws \InvalidArgumentException if an element in $arrayToTest is not in $allowedArrayKeys
31
     * @internal
32
     */
33
    public static function assertAllArrayKeysAreValid(array $arrayToTest, array $allowedArrayKeys)
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
48
    /**
49
     * Recursively convert 'true' and 'false' strings to boolean values.
50
     *
51
     * @param array $array
52
     * @return array the modified array
53
     */
54
    public static function convertBooleanStringsToBooleanRecursive(array $array): array
55
    {
56
        $result = $array;
57
        foreach ($result as $key => $value) {
58
            if (is_array($value)) {
59
                $result[$key] = self::convertBooleanStringsToBooleanRecursive($value);
60
            } else {
61
                if ($value === 'true') {
62
                    $result[$key] = true;
63
                } elseif ($value === 'false') {
64
                    $result[$key] = false;
65
                }
66
            }
67
        }
68
        return $result;
69
    }
70
71
    /**
72
     * Reduce an array by a search value and keep the array structure.
73
     *
74
     * Comparison is type strict:
75
     * - For a given needle of type string, integer, array or boolean,
76
     * value and value type must match to occur in result array
77
     * - For a given object, an object within the array must be a reference to
78
     * the same object to match (not just different instance of same class)
79
     *
80
     * Example:
81
     * - Needle: 'findMe'
82
     * - Given array:
83
     * array(
84
     *   'foo' => 'noMatch',
85
     *   'bar' => 'findMe',
86
     *   'foobar => array(
87
     *     'foo' => 'findMe',
88
     *   ),
89
     * );
90
     * - Result:
91
     * array(
92
     *   'bar' => 'findMe',
93
     *   'foobar' => array(
94
     *     'foo' => findMe',
95
     *   ),
96
     * );
97
     *
98
     * See the unit tests for more examples and expected behaviour
99
     *
100
     * @param mixed $needle The value to search for
101
     * @param array $haystack The array in which to search
102
     * @return array $haystack array reduced matching $needle values
103
     */
104
    public static function filterByValueRecursive($needle = '', array $haystack = [])
105
    {
106
        $resultArray = [];
107
        // Define a lambda function to be applied to all members of this array dimension
108
        // Call recursive if current value is of type array
109
        // Write to $resultArray (by reference!) if types and value match
110
        $callback = function (&$value, $key) use ($needle, &$resultArray) {
111
            if ($value === $needle) {
112
                $resultArray[$key] = $value;
113
            } elseif (is_array($value)) {
114
                $subArrayMatches = static::filterByValueRecursive($needle, $value);
115
                if (!empty($subArrayMatches)) {
116
                    $resultArray[$key] = $subArrayMatches;
117
                }
118
            }
119
        };
120
        // array_walk() is not affected by the internal pointers, no need to reset
121
        array_walk($haystack, $callback);
122
        // Pointers to result array are reset internally
123
        return $resultArray;
124
    }
125
126
    /**
127
     * Checks if a given path exists in array
128
     *
129
     * Example:
130
     * - array:
131
     * array(
132
     *   'foo' => array(
133
     *     'bar' = 'test',
134
     *   )
135
     * );
136
     * - path: 'foo/bar'
137
     * - return: TRUE
138
     *
139
     * @param array $array Given array
140
     * @param string $path Path to test, 'foo/bar/foobar'
141
     * @param string $delimiter Delimiter for path, default /
142
     * @return bool TRUE if path exists in array
143
     */
144
    public static function isValidPath(array $array, $path, $delimiter = '/')
145
    {
146
        $isValid = true;
147
        try {
148
            static::getValueByPath($array, $path, $delimiter);
149
        } catch (MissingArrayPathException $e) {
150
            $isValid = false;
151
        }
152
        return $isValid;
153
    }
154
155
    /**
156
     * Returns a value by given path
157
     *
158
     * Example
159
     * - array:
160
     * array(
161
     *   'foo' => array(
162
     *     'bar' => array(
163
     *       'baz' => 42
164
     *     )
165
     *   )
166
     * );
167
     * - path: foo/bar/baz
168
     * - return: 42
169
     *
170
     * If a path segments contains a delimiter character, the path segment
171
     * must be enclosed by " (double quote), see unit tests for details
172
     *
173
     * @param array $array Input array
174
     * @param array|string $path Path within the array
175
     * @param string $delimiter Defined path delimiter, default /
176
     * @return mixed
177
     * @throws \RuntimeException if the path is empty, or if the path does not exist
178
     * @throws \InvalidArgumentException if the path is neither array nor string
179
     */
180
    public static function getValueByPath(array $array, $path, $delimiter = '/')
181
    {
182
        // Extract parts of the path
183
        if (is_string($path)) {
184
            if ($path === '') {
185
                // Programming error has to be sanitized before calling the method -> global exception
186
                throw new \RuntimeException('Path must not be empty', 1341397767);
187
            }
188
            $path = str_getcsv($path, $delimiter);
189
        } elseif (!is_array($path)) {
0 ignored issues
show
introduced by
The condition is_array($path) is always true.
Loading history...
190
            // Programming error has to be sanitized before calling the method -> global exception
191
            throw new \InvalidArgumentException('getValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1476557628);
192
        }
193
        // Loop through each part and extract its value
194
        $value = $array;
195
        foreach ($path as $segment) {
196
            if (is_array($value) && array_key_exists($segment, $value)) {
197
                // Replace current value with child
198
                $value = $value[$segment];
199
            } else {
200
                // Throw specific exception if there is no such path
201
                throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1341397869);
202
            }
203
        }
204
        return $value;
205
    }
206
207
    /**
208
     * Reindex keys from the current nesting level if all keys within
209
     * the current nesting level are integers.
210
     *
211
     * @param array $array
212
     * @return array
213
     */
214
    public static function reIndexNumericArrayKeysRecursive(array $array): array
215
    {
216
        if (count(array_filter(array_keys($array), 'is_string')) === 0) {
217
            $array = array_values($array);
218
        }
219
        foreach ($array as $key => $value) {
220
            if (is_array($value) && !empty($value)) {
221
                $array[$key] = self::reIndexNumericArrayKeysRecursive($value);
222
            }
223
        }
224
        return $array;
225
    }
226
227
    /**
228
     * Recursively remove keys if their value are NULL.
229
     *
230
     * @param array $array
231
     * @return array the modified array
232
     */
233
    public static function removeNullValuesRecursive(array $array): array
234
    {
235
        $result = $array;
236
        foreach ($result as $key => $value) {
237
            if (is_array($value)) {
238
                $result[$key] = self::removeNullValuesRecursive($value);
239
            } elseif ($value === null) {
240
                unset($result[$key]);
241
            }
242
        }
243
        return $result;
244
    }
245
246
    /**
247
     * Modifies or sets a new value in an array by given path
248
     *
249
     * Example:
250
     * - array:
251
     * array(
252
     *   'foo' => array(
253
     *     'bar' => 42,
254
     *   ),
255
     * );
256
     * - path: foo/bar
257
     * - value: 23
258
     * - return:
259
     * array(
260
     *   'foo' => array(
261
     *     'bar' => 23,
262
     *   ),
263
     * );
264
     *
265
     * @param array $array Input array to manipulate
266
     * @param string|array|\ArrayAccess $path Path in array to search for
267
     * @param mixed $value Value to set at path location in array
268
     * @param string $delimiter Path delimiter
269
     * @return array Modified array
270
     * @throws \RuntimeException
271
     */
272
    public static function setValueByPath(array $array, $path, $value, $delimiter = '/')
273
    {
274
        if (is_string($path)) {
275
            if ($path === '') {
276
                throw new \RuntimeException('Path must not be empty', 1341406194);
277
            }
278
            // Extract parts of the path
279
            $path = str_getcsv($path, $delimiter);
280
        } elseif (!is_array($path) && !$path instanceof \ArrayAccess) {
0 ignored issues
show
introduced by
$path is always a sub-type of ArrayAccess.
Loading history...
281
            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);
282
        }
283
        // Point to the root of the array
284
        $pointer = &$array;
285
        // Find path in given array
286
        foreach ($path as $segment) {
287
            // Fail if the part is empty
288
            if ($segment === '') {
289
                throw new \RuntimeException('Invalid path segment specified', 1341406846);
290
            }
291
            // Create cell if it doesn't exist
292
            if (!array_key_exists($segment, $pointer)) {
293
                $pointer[$segment] = [];
294
            }
295
            // Set pointer to new cell
296
            $pointer = &$pointer[$segment];
297
        }
298
        // Set value of target cell
299
        $pointer = $value;
300
        return $array;
301
    }
302
303
    /**
304
     * Remove a sub part from an array specified by path
305
     *
306
     * @param array $array Input array to manipulate
307
     * @param string $path Path to remove from array
308
     * @param string $delimiter Path delimiter
309
     * @return array Modified array
310
     * @throws \RuntimeException
311
     */
312
    public static function removeByPath(array $array, $path, $delimiter = '/')
313
    {
314
        if (!is_string($path)) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
315
            throw new \RuntimeException('Path must be a string', 1371757719);
316
        }
317
        if ($path === '') {
318
            throw new \RuntimeException('Path must not be empty', 1371757718);
319
        }
320
        // Extract parts of the path
321
        $path = str_getcsv($path, $delimiter);
322
        $pathDepth = count($path);
323
        $currentDepth = 0;
324
        $pointer = &$array;
325
        // Find path in given array
326
        foreach ($path as $segment) {
327
            $currentDepth++;
328
            // Fail if the part is empty
329
            if ($segment === '') {
330
                throw new \RuntimeException('Invalid path segment specified', 1371757720);
331
            }
332
            if (!array_key_exists($segment, $pointer)) {
333
                throw new MissingArrayPathException('Segment ' . $segment . ' of path ' . implode($delimiter, $path) . ' does not exist in array', 1371758436);
334
            }
335
            if ($currentDepth === $pathDepth) {
336
                unset($pointer[$segment]);
337
            } else {
338
                $pointer = &$pointer[$segment];
339
            }
340
        }
341
        return $array;
342
    }
343
344
    /**
345
     * Sorts an array recursively by key
346
     *
347
     * @param array $array Array to sort recursively by key
348
     * @return array Sorted array
349
     */
350
    public static function sortByKeyRecursive(array $array)
351
    {
352
        ksort($array);
353
        foreach ($array as $key => $value) {
354
            if (is_array($value) && !empty($value)) {
355
                $array[$key] = self::sortByKeyRecursive($value);
356
            }
357
        }
358
        return $array;
359
    }
360
361
    /**
362
     * Sort an array of arrays by a given key using uasort
363
     *
364
     * @param array $arrays Array of arrays to sort
365
     * @param string $key Key to sort after
366
     * @param bool $ascending Set to TRUE for ascending order, FALSE for descending order
367
     * @return array Array of sorted arrays
368
     * @throws \RuntimeException
369
     */
370
    public static function sortArraysByKey(array $arrays, $key, $ascending = true)
371
    {
372
        if (empty($arrays)) {
373
            return $arrays;
374
        }
375
        $sortResult = uasort($arrays, function (array $a, array $b) use ($key, $ascending) {
376
            if (!isset($a[$key]) || !isset($b[$key])) {
377
                throw new \RuntimeException('The specified sorting key "' . $key . '" is not available in the given array.', 1373727309);
378
            }
379
            return $ascending ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
380
        });
381
        if (!$sortResult) {
382
            throw new \RuntimeException('The function uasort() failed for unknown reasons.', 1373727329);
383
        }
384
        return $arrays;
385
    }
386
387
    /**
388
     * Exports an array as string.
389
     * Similar to var_export(), but representation follows the PSR-2 and TYPO3 core CGL.
390
     *
391
     * See unit tests for detailed examples
392
     *
393
     * @param array $array Array to export
394
     * @param int $level Internal level used for recursion, do *not* set from outside!
395
     * @return string String representation of array
396
     * @throws \RuntimeException
397
     */
398
    public static function arrayExport(array $array = [], $level = 0)
399
    {
400
        $lines = "[\n";
401
        $level++;
402
        $writeKeyIndex = false;
403
        $expectedKeyIndex = 0;
404
        foreach ($array as $key => $value) {
405
            if ($key === $expectedKeyIndex) {
406
                $expectedKeyIndex++;
407
            } else {
408
                // Found a non integer or non consecutive key, so we can break here
409
                $writeKeyIndex = true;
410
                break;
411
            }
412
        }
413
        foreach ($array as $key => $value) {
414
            // Indention
415
            $lines .= str_repeat('    ', $level);
416
            if ($writeKeyIndex) {
417
                // Numeric / string keys
418
                $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
419
            }
420
            if (is_array($value)) {
421
                if (!empty($value)) {
422
                    $lines .= self::arrayExport($value, $level);
423
                } else {
424
                    $lines .= "[],\n";
425
                }
426
            } elseif (is_int($value) || is_float($value)) {
427
                $lines .= $value . ",\n";
428
            } elseif ($value === null) {
429
                $lines .= "null,\n";
430
            } elseif (is_bool($value)) {
431
                $lines .= $value ? 'true' : 'false';
432
                $lines .= ",\n";
433
            } elseif (is_string($value)) {
434
                // Quote \ to \\
435
                // Quote ' to \'
436
                $stringContent = str_replace(['\\', '\''], ['\\\\', '\\\''], $value);
437
                $lines .= '\'' . $stringContent . "',\n";
438
            } else {
439
                throw new \RuntimeException('Objects are not supported', 1342294987);
440
            }
441
        }
442
        $lines .= str_repeat('    ', $level - 1) . ']' . ($level - 1 == 0 ? '' : ",\n");
443
        return $lines;
444
    }
445
446
    /**
447
     * Converts a multidimensional array to a flat representation.
448
     *
449
     * See unit tests for more details
450
     *
451
     * Example:
452
     * - array:
453
     * array(
454
     *   'first.' => array(
455
     *     'second' => 1
456
     *   )
457
     * )
458
     * - result:
459
     * array(
460
     *   'first.second' => 1
461
     * )
462
     *
463
     * Example:
464
     * - array:
465
     * array(
466
     *   'first' => array(
467
     *     'second' => 1
468
     *   )
469
     * )
470
     * - result:
471
     * array(
472
     *   'first.second' => 1
473
     * )
474
     *
475
     * @param array $array The (relative) array to be converted
476
     * @param string $prefix The (relative) prefix to be used (e.g. 'section.')
477
     * @param bool $keepDots
478
     * @return array
479
     */
480
    public static function flatten(array $array, $prefix = '', bool $keepDots = false)
481
    {
482
        $flatArray = [];
483
        foreach ($array as $key => $value) {
484
            if ($keepDots === false) {
485
                // Ensure there is no trailing dot:
486
                $key = rtrim($key, '.');
487
            }
488
            if (!is_array($value)) {
489
                $flatArray[$prefix . $key] = $value;
490
            } else {
491
                $newPrefix = $prefix . $key;
492
                if ($keepDots === false) {
493
                    $newPrefix = $prefix . $key . '.';
494
                }
495
                $flatArray = array_merge($flatArray, self::flatten($value, $newPrefix, $keepDots));
496
            }
497
        }
498
        return $flatArray;
499
    }
500
501
    /**
502
     * Determine the intersections between two arrays, recursively comparing keys
503
     * A complete sub array of $source will be preserved, if the key exists in $mask.
504
     *
505
     * See unit tests for more examples and edge cases.
506
     *
507
     * Example:
508
     * - source:
509
     * array(
510
     *   'key1' => 'bar',
511
     *   'key2' => array(
512
     *     'subkey1' => 'sub1',
513
     *     'subkey2' => 'sub2',
514
     *   ),
515
     *   'key3' => 'baz',
516
     * )
517
     * - mask:
518
     * array(
519
     *   'key1' => NULL,
520
     *   'key2' => array(
521
     *     'subkey1' => exists',
522
     *   ),
523
     * )
524
     * - return:
525
     * array(
526
     *   'key1' => 'bar',
527
     *   'key2' => array(
528
     *     'subkey1' => 'sub1',
529
     *   ),
530
     * )
531
     *
532
     * @param array $source Source array
533
     * @param array $mask Array that has the keys which should be kept in the source array
534
     * @return array Keys which are present in both arrays with values of the source array
535
     */
536
    public static function intersectRecursive(array $source, array $mask = [])
537
    {
538
        $intersection = [];
539
        foreach ($source as $key => $_) {
540
            if (!array_key_exists($key, $mask)) {
541
                continue;
542
            }
543
            if (is_array($source[$key]) && is_array($mask[$key])) {
544
                $value = self::intersectRecursive($source[$key], $mask[$key]);
545
                if (!empty($value)) {
546
                    $intersection[$key] = $value;
547
                }
548
            } else {
549
                $intersection[$key] = $source[$key];
550
            }
551
        }
552
        return $intersection;
553
    }
554
555
    /**
556
     * Renumber the keys of an array to avoid leaps if keys are all numeric.
557
     *
558
     * Is called recursively for nested arrays.
559
     *
560
     * Example:
561
     *
562
     * Given
563
     *  array(0 => 'Zero' 1 => 'One', 2 => 'Two', 4 => 'Three')
564
     * as input, it will return
565
     *  array(0 => 'Zero' 1 => 'One', 2 => 'Two', 3 => 'Three')
566
     *
567
     * Will treat keys string representations of number (ie. '1') equal to the
568
     * numeric value (ie. 1).
569
     *
570
     * Example:
571
     * Given
572
     *  array('0' => 'Zero', '1' => 'One' )
573
     * it will return
574
     *  array(0 => 'Zero', 1 => 'One')
575
     *
576
     * @param array $array Input array
577
     * @param int $level Internal level used for recursion, do *not* set from outside!
578
     * @return array
579
     */
580
    public static function renumberKeysToAvoidLeapsIfKeysAreAllNumeric(array $array = [], $level = 0)
581
    {
582
        $level++;
583
        $allKeysAreNumeric = true;
584
        foreach ($array as $key => $_) {
585
            if (is_int($key) === false) {
586
                $allKeysAreNumeric = false;
587
                break;
588
            }
589
        }
590
        $renumberedArray = $array;
591
        if ($allKeysAreNumeric === true) {
592
            $renumberedArray = array_values($array);
593
        }
594
        foreach ($renumberedArray as $key => $value) {
595
            if (is_array($value)) {
596
                $renumberedArray[$key] = self::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($value, $level);
597
            }
598
        }
599
        return $renumberedArray;
600
    }
601
602
    /**
603
     * Merges two arrays recursively and "binary safe" (integer keys are
604
     * overridden as well), overruling similar values in the original array
605
     * with the values of the overrule array.
606
     * In case of identical keys, ie. keeping the values of the overrule array.
607
     *
608
     * This method takes the original array by reference for speed optimization with large arrays
609
     *
610
     * The differences to the existing PHP function array_merge_recursive() are:
611
     *  * Keys of the original array can be unset via the overrule array. ($enableUnsetFeature)
612
     *  * Much more control over what is actually merged. ($addKeys, $includeEmptyValues)
613
     *  * Elements or the original array get overwritten if the same key is present in the overrule array.
614
     *
615
     * @param array $original Original array. It will be *modified* by this method and contains the result afterwards!
616
     * @param array $overrule Overrule array, overruling the original array
617
     * @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.
618
     * @param bool $includeEmptyValues If set, values from $overrule will overrule if they are empty or zero.
619
     * @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.
620
     */
621
    public static function mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
622
    {
623
        foreach ($overrule as $key => $_) {
624
            if ($enableUnsetFeature && $overrule[$key] === '__UNSET') {
625
                unset($original[$key]);
626
                continue;
627
            }
628
            if (isset($original[$key]) && is_array($original[$key])) {
629
                if (is_array($overrule[$key])) {
630
                    self::mergeRecursiveWithOverrule($original[$key], $overrule[$key], $addKeys, $includeEmptyValues, $enableUnsetFeature);
631
                }
632
            } elseif (
633
                ($addKeys || isset($original[$key])) &&
634
                ($includeEmptyValues || $overrule[$key])
635
            ) {
636
                $original[$key] = $overrule[$key];
637
            }
638
        }
639
        // This line is kept for backward compatibility reasons.
640
        reset($original);
641
    }
642
643
    /**
644
     * Removes the value $cmpValue from the $array if found there. Returns the modified array
645
     *
646
     * @param array $array Array containing the values
647
     * @param string $cmpValue Value to search for and if found remove array entry where found.
648
     * @return array Output array with entries removed if search string is found
649
     */
650
    public static function removeArrayEntryByValue(array $array, $cmpValue)
651
    {
652
        foreach ($array as $k => $v) {
653
            if (is_array($v)) {
654
                $array[$k] = self::removeArrayEntryByValue($v, $cmpValue);
655
            } elseif ((string)$v === (string)$cmpValue) {
656
                unset($array[$k]);
657
            }
658
        }
659
        return $array;
660
    }
661
662
    /**
663
     * Filters an array to reduce its elements to match the condition.
664
     * The values in $keepItems can be optionally evaluated by a custom callback function.
665
     *
666
     * Example (arguments used to call this function):
667
     * $array = array(
668
     * array('aa' => array('first', 'second'),
669
     * array('bb' => array('third', 'fourth'),
670
     * array('cc' => array('fifth', 'sixth'),
671
     * );
672
     * $keepItems = array('third');
673
     * $getValueFunc = function($value) { return $value[0]; }
674
     *
675
     * Returns:
676
     * array(
677
     * array('bb' => array('third', 'fourth'),
678
     * )
679
     *
680
     * @param array $array The initial array to be filtered/reduced
681
     * @param mixed $keepItems The items which are allowed/kept in the array - accepts array or csv string
682
     * @param callable|null $getValueFunc (optional) Callback function used to get the value to keep
683
     * @return array The filtered/reduced array with the kept items
684
     */
685
    public static function keepItemsInArray(array $array, $keepItems, $getValueFunc = null)
686
    {
687
        if ($array) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $array of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
688
            // Convert strings to arrays:
689
            if (is_string($keepItems)) {
690
                $keepItems = GeneralUtility::trimExplode(',', $keepItems);
691
            }
692
            // Check if valueFunc can be executed:
693
            if (!is_callable($getValueFunc)) {
694
                $getValueFunc = null;
695
            }
696
            // Do the filtering:
697
            if (is_array($keepItems) && !empty($keepItems)) {
698
                $keepItems = array_flip($keepItems);
699
                foreach ($array as $key => $value) {
700
                    // Get the value to compare by using the callback function:
701
                    $keepValue = isset($getValueFunc) ? $getValueFunc($value) : $value;
702
                    if (!isset($keepItems[$keepValue])) {
703
                        unset($array[$key]);
704
                    }
705
                }
706
            }
707
        }
708
        return $array;
709
    }
710
711
    /**
712
     * Rename Array keys with a given mapping table
713
     *
714
     * @param array	$array Array by reference which should be remapped
715
     * @param array	$mappingTable Array with remap information, array/$oldKey => $newKey)
716
     */
717
    public static function remapArrayKeys(array &$array, array $mappingTable)
718
    {
719
        foreach ($mappingTable as $old => $new) {
720
            if ($new && isset($array[$old])) {
721
                $array[$new] = $array[$old];
722
                unset($array[$old]);
723
            }
724
        }
725
    }
726
727
    /**
728
     * Filters keys off from first array that also exist in second array. Comparison is done by keys.
729
     * This method is a recursive version of php array_diff_key()
730
     *
731
     * @param array $array1 Source array
732
     * @param array $array2 Reduce source array by this array
733
     * @return array Source array reduced by keys also present in second array
734
     */
735
    public static function arrayDiffKeyRecursive(array $array1, array $array2): array
736
    {
737
        $differenceArray = [];
738
        foreach ($array1 as $key => $value) {
739
            if (!array_key_exists($key, $array2)) {
740
                $differenceArray[$key] = $value;
741
            } elseif (is_array($value)) {
742
                if (is_array($array2[$key])) {
743
                    $recursiveResult = self::arrayDiffKeyRecursive($value, $array2[$key]);
744
                    if (!empty($recursiveResult)) {
745
                        $differenceArray[$key] = $recursiveResult;
746
                    }
747
                }
748
            }
749
        }
750
        return $differenceArray;
751
    }
752
753
    /**
754
     * Filters values off from first array that also exist in second array. Comparison is done by keys.
755
     * This method is a recursive version of php array_diff_assoc()
756
     *
757
     * @param array $array1 Source array
758
     * @param array $array2 Reduce source array by this array
759
     * @param bool $useArrayDiffAssocBehavior If false, the old array_diff_key() behavior is kept and a deprecation warning is triggered. Will be removed in TYPO3 v12.
760
     * @return array Source array reduced by values also present in second array, indexed by key
761
     */
762
    public static function arrayDiffAssocRecursive(array $array1, array $array2, bool $useArrayDiffAssocBehavior = false): array
763
    {
764
        if (!$useArrayDiffAssocBehavior) {
765
            trigger_error(
766
                sprintf(
767
                    'Using the array_diff_key() behavior of %1$s is deprecated, use ArrayUtility::arrayDiffKeyRecursive() instead.'
768
                    . ' Set the 3rd parameter of %1$s to true to switch to array_diff_assoc(), which will become the default behavior in TYPO3 v12.',
769
                    __METHOD__
770
                ),
771
                E_USER_DEPRECATED
772
            );
773
            return self::arrayDiffKeyRecursive($array1, $array2);
774
        }
775
776
        $differenceArray = [];
777
        foreach ($array1 as $key => $value) {
778
            if (!array_key_exists($key, $array2) || (!is_array($value) && $value !== $array2[$key])) {
779
                $differenceArray[$key] = $value;
780
            } elseif (is_array($value)) {
781
                if (is_array($array2[$key])) {
782
                    $recursiveResult = self::arrayDiffAssocRecursive($value, $array2[$key], $useArrayDiffAssocBehavior);
783
                    if (!empty($recursiveResult)) {
784
                        $differenceArray[$key] = $recursiveResult;
785
                    }
786
                }
787
            }
788
        }
789
        return $differenceArray;
790
    }
791
792
    /**
793
     * Sorts an array by key recursive - uses natural sort order (aAbB-zZ)
794
     *
795
     * @param array $array array to be sorted recursively, passed by reference
796
     * @return bool always TRUE
797
     */
798
    public static function naturalKeySortRecursive(array &$array)
799
    {
800
        uksort($array, 'strnatcasecmp');
801
        foreach ($array as $key => &$value) {
802
            if (is_array($value)) {
803
                self::naturalKeySortRecursive($value);
804
            }
805
        }
806
807
        return true;
808
    }
809
810
    /**
811
     * 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.
812
     *
813
     * @param array $setupArr TypoScript array with numerical array in
814
     * @param bool $acceptAnyKeys If set, then a value is not required - the properties alone will be enough.
815
     * @return array An array with all integer properties listed in numeric order.
816
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGet()
817
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder
818
     */
819
    public static function filterAndSortByNumericKeys($setupArr, $acceptAnyKeys = false)
820
    {
821
        $filteredKeys = [];
822
        $keys = array_keys($setupArr);
823
        foreach ($keys as $key) {
824
            if ($acceptAnyKeys || MathUtility::canBeInterpretedAsInteger($key)) {
825
                $filteredKeys[] = (int)$key;
826
            }
827
        }
828
        $filteredKeys = array_unique($filteredKeys);
829
        sort($filteredKeys);
830
        return $filteredKeys;
831
    }
832
833
    /**
834
     * If the array contains numerical keys only, sort it in ascending order
835
     *
836
     * @param array $array
837
     *
838
     * @return array
839
     */
840
    public static function sortArrayWithIntegerKeys(array $array)
841
    {
842
        if (count(array_filter(array_keys($array), 'is_string')) === 0) {
843
            ksort($array);
844
        }
845
        return $array;
846
    }
847
848
    /**
849
     * Sort keys from the current nesting level if all keys within the
850
     * current nesting level are integers.
851
     *
852
     * @param array $array
853
     * @return array
854
     */
855
    public static function sortArrayWithIntegerKeysRecursive(array $array): array
856
    {
857
        $array = static::sortArrayWithIntegerKeys($array);
858
        foreach ($array as $key => $value) {
859
            if (is_array($value) && !empty($value)) {
860
                $array[$key] = self::sortArrayWithIntegerKeysRecursive($value);
861
            }
862
        }
863
        return $array;
864
    }
865
866
    /**
867
     * Recursively translate values.
868
     *
869
     * @param array $array
870
     * @return array the modified array
871
     */
872
    public static function stripTagsFromValuesRecursive(array $array): array
873
    {
874
        $result = $array;
875
        foreach ($result as $key => $value) {
876
            if (is_array($value)) {
877
                $result[$key] = self::stripTagsFromValuesRecursive($value);
878
            } elseif (is_string($value) || (is_object($value) && method_exists($value, '__toString'))) {
879
                $result[$key] = strip_tags((string)$value);
880
            }
881
        }
882
        return $result;
883
    }
884
885
    /**
886
     * Recursively filter an array
887
     *
888
     * @param array $array
889
     * @param callable|null $callback
890
     * @return array the filtered array
891
     * @see https://secure.php.net/manual/en/function.array-filter.php
892
     */
893
    public static function filterRecursive(array $array, callable $callback = null): array
894
    {
895
        $callback = $callback ?: function ($value) {
896
            return (bool)$value;
897
        };
898
899
        foreach ($array as $key => $value) {
900
            if (is_array($value)) {
901
                $array[$key] = self::filterRecursive($value, $callback);
902
            }
903
904
            if (!$callback($value)) {
905
                unset($array[$key]);
906
            }
907
        }
908
909
        return $array;
910
    }
911
912
    /**
913
     * Check whether the array has non-integer keys. If there is at least one string key, $array will be
914
     * regarded as an associative array.
915
     *
916
     * @param array $array
917
     * @return bool True in case a string key was found.
918
     * @internal
919
     */
920
    public static function isAssociative(array $array): bool
921
    {
922
        return count(array_filter(array_keys($array), 'is_string')) > 0;
923
    }
924
925
    /**
926
     * Same as array_replace_recursive except that when in simple arrays (= YAML lists), the entries are
927
     * appended (array_merge). The second array takes precedence in case of equal sub arrays.
928
     *
929
     * @param array $array1
930
     * @param array $array2
931
     * @return array
932
     * @internal
933
     */
934
    public static function replaceAndAppendScalarValuesRecursive(array $array1, array $array2): array
935
    {
936
        // Simple lists get merged / added up
937
        if (!self::isAssociative($array1)) {
938
            return array_merge($array1, $array2);
939
        }
940
        foreach ($array1 as $k => $v) {
941
            // The key also exists in second array, if it is a simple value
942
            // then $array2 will override the value, where an array is calling
943
            // replaceAndAppendScalarValuesRecursive() recursively.
944
            if (isset($array2[$k])) {
945
                if (is_array($v) && is_array($array2[$k])) {
946
                    $array1[$k] = self::replaceAndAppendScalarValuesRecursive($v, $array2[$k]);
947
                } else {
948
                    $array1[$k] = $array2[$k];
949
                }
950
                unset($array2[$k]);
951
            }
952
        }
953
        // If there are properties in the second array left, they are added up
954
        if (!empty($array2)) {
955
            foreach ($array2 as $k => $v) {
956
                $array1[$k] = $v;
957
            }
958
        }
959
960
        return $array1;
961
    }
962
}
963