Issues (25)

src/ArrayHelper.php (13 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Arrays;
6
7
use Closure;
8
use InvalidArgumentException;
9
use Throwable;
10
use Yiisoft\Strings\NumericHelper;
11
use Yiisoft\Strings\StringHelper;
12
13
use function array_key_exists;
14
use function count;
15
use function gettype;
16
use function in_array;
17
use function is_array;
18
use function is_float;
19
use function is_int;
20
use function is_object;
21
use function is_string;
22
23
/**
24
 * Yii array helper provides static methods allowing you to deal with arrays more efficiently.
25
 *
26
 * @psalm-type ArrayKey = float|int|string|array<array-key,float|int|string>
27
 * @psalm-type ArrayPath = float|int|string|array<array-key,float|int|string|array<array-key,float|int|string>>
28
 */
29
final class ArrayHelper
30
{
31
    /**
32
     * Converts an object or an array of objects into an array.
33
     *
34
     * For example:
35
     *
36
     * ```php
37
     * [
38
     *     Post::class => [
39
     *         'id',
40
     *         'title',
41
     *         'createTime' => 'created_at',
42
     *         'length' => function ($post) {
43
     *             return strlen($post->content);
44
     *         },
45
     *     ],
46
     * ]
47
     * ```
48
     *
49
     * The result of `ArrayHelper::toArray($post, $properties)` could be like the following:
50
     *
51
     * ```php
52
     * [
53
     *     'id' => 123,
54
     *     'title' => 'test',
55
     *     'createTime' => '2013-01-01 12:00AM',
56
     *     'length' => 301,
57
     * ]
58
     * ```
59
     *
60
     * @param mixed $object The object to be converted into an array.
61
     *
62
     * It is possible to provide default way of converting object to array for a specific class by implementing
63
     * {@see \Yiisoft\Arrays\ArrayableInterface} in its class.
64
     * @param array $properties A mapping from object class names to the properties that need to put into
65
     * the resulting arrays. The properties specified for each class is an array of the following format:
66
     *
67
     * - A field name to include as is.
68
     * - A key-value pair of desired array key name and model column name to take value from.
69
     * - A key-value pair of desired array key name and a callback which returns value.
70
     * @param bool $recursive Whether to recursively converts properties which are objects into arrays.
71
     *
72
     * @return array The array representation of the object.
73
     */
74 6
    public static function toArray(mixed $object, array $properties = [], bool $recursive = true): array
75
    {
76 6
        if (is_array($object)) {
77 5
            if ($recursive) {
78 4
                foreach ($object as $key => $value) {
79 4
                    if (is_array($value) || is_object($value)) {
80 4
                        $object[$key] = self::toArray($value, $properties);
81
                    }
82
                }
83
            }
84
85 5
            return $object;
86
        }
87
88 5
        if (is_object($object)) {
89 5
            if (!empty($properties)) {
90 1
                $className = $object::class;
91 1
                if (!empty($properties[$className])) {
92 1
                    $result = [];
93
                    /**
94
                     * @var int|string $key
95
                     * @var string $name
96
                     */
97 1
                    foreach ($properties[$className] as $key => $name) {
98 1
                        if (is_int($key)) {
99 1
                            $result[$name] = $object->$name;
100
                        } else {
101 1
                            $result[$key] = self::getValue($object, $name);
102
                        }
103
                    }
104
105 1
                    return $recursive ? self::toArray($result, $properties) : $result;
106
                }
107
            }
108 5
            if ($object instanceof ArrayableInterface) {
109 4
                $result = $object->toArray([], [], $recursive);
110
            } else {
111 4
                $result = [];
112
                /**
113
                 * @var string $key
114
                 */
115 4
                foreach ($object as $key => $value) {
116 4
                    $result[$key] = $value;
117
                }
118
            }
119
120 5
            return $recursive ? self::toArray($result, $properties) : $result;
121
        }
122
123 1
        return [$object];
124
    }
125
126
    /**
127
     * Merges two or more arrays into one recursively. If each array has an element with the same string key value,
128
     * the latter will overwrite the former (different from {@see array_merge_recursive()}). Recursive merging will be
129
     * conducted if both arrays have an element of array type and are having the same key. For integer-keyed elements,
130
     * the elements from the latter array will be appended to the former array.
131
     *
132
     * @param array ...$arrays Arrays to be merged.
133
     *
134
     * @return array The merged array (the original arrays are not changed).
135
     */
136 4
    public static function merge(...$arrays): array
137
    {
138 4
        return self::doMerge($arrays, null);
139
    }
140
141
    /**
142
     * Merges two or more arrays into one recursively with specified depth. If each array has an element with the same
143
     * string key value, the latter will overwrite the former (different from {@see array_merge_recursive()}).
144
     * Recursive merging will be conducted if both arrays have an element of array type and are having the same key.
145
     * For integer-keyed elements, the elements from the latter array will be appended to the former array.
146
     *
147
     * @param array[] $arrays Arrays to be merged.
148
     * @param int|null $depth The maximum depth that merging is recursively. `Null` means unlimited depth.
149
     *
150
     * @return array The merged array (the original arrays are not changed).
151
     */
152 5
    public static function parametrizedMerge(array $arrays, ?int $depth): array
153
    {
154 5
        return self::doMerge($arrays, $depth);
155
    }
156
157
    /**
158
     * Retrieves the value of an array element or object property with the given key or property name.
159
     * If the key does not exist in the array or object, the default value will be returned instead.
160
     *
161
     * Below are some usage examples,
162
     *
163
     * ```php
164
     * // Working with array:
165
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($_POST, 'username');
166
     *
167
     * // Working with object:
168
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($user, 'username');
169
     *
170
     * // Working with anonymous function:
171
     * $fullName = \Yiisoft\Arrays\ArrayHelper::getValue($user, function ($user, $defaultValue) {
172
     *     return $user->firstName . ' ' . $user->lastName;
173
     * });
174
     *
175
     * // Using an array of keys to retrieve the value:
176
     * $value = \Yiisoft\Arrays\ArrayHelper::getValue($versions, ['1.0', 'date']);
177
     * ```
178
     *
179
     * @param array|object $array Array or object to extract value from.
180
     * @param array|Closure|float|int|string $key Key name of the array element,
181
     * an array of keys, object property name, object method like `getName()`, or an anonymous function
182
     * returning the value. The anonymous function signature should be:
183
     * `function($array, $defaultValue)`.
184
     * @param mixed $default The default value to be returned if the specified array key does not exist. Not used when
185
     * getting value from an object.
186
     *
187
     * @psalm-param ArrayKey|Closure $key
188
     *
189
     * @return mixed The value of the element if found, default value otherwise.
190
     */
191 95
    public static function getValue(
192
        array|object $array,
193
        array|Closure|float|int|string $key,
194
        mixed $default = null
195
    ): mixed {
196 95
        if ($key instanceof Closure) {
0 ignored issues
show
$key is never a sub-type of Closure.
Loading history...
197 16
            return $key($array, $default);
198
        }
199
200 87
        if (is_array($key)) {
201
            /** @psalm-var array<mixed,string|int> $key */
202 42
            $lastKey = array_pop($key);
203 42
            foreach ($key as $keyPart) {
204 39
                $array = self::getRootValue($array, $keyPart, null);
205 39
                if (!is_array($array) && !is_object($array)) {
206 10
                    return $default;
207
                }
208
            }
209 33
            return self::getRootValue($array, $lastKey, $default);
210
        }
211
212 47
        return self::getRootValue($array, $key, $default);
213
    }
214
215
    /**
216
     * @param mixed $array Array or object to extract value from, otherwise method will return $default.
217
     * @param float|int|string $key Key name of the array element, object property name or object method like `getValue()`.
218
     * @param mixed $default The default value to be returned if the specified array key does not exist. Not used when
219
     * getting value from an object.
220
     *
221
     * @return mixed The value of the element if found, default value otherwise.
222
     */
223 114
    private static function getRootValue(mixed $array, float|int|string $key, mixed $default): mixed
224
    {
225 114
        if (is_array($array)) {
226 101
            $key = self::normalizeArrayKey($key);
227 101
            return array_key_exists($key, $array) ? $array[$key] : $default;
228
        }
229
230 15
        if (is_object($array)) {
231 15
            $key = (string) $key;
232
233 15
            if (str_ends_with($key, '()')) {
234 1
                $method = substr($key, 0, -2);
235
                /** @psalm-suppress MixedMethodCall */
236 1
                return $array->$method();
237
            }
238
239
            try {
240
                /** @psalm-suppress MixedPropertyFetch */
241 14
                return $array::$$key;
242 14
            } catch (Throwable) {
243
                /**
244
                 * This is expected to fail if the property does not exist, or __get() is not implemented.
245
                 * It is not reliably possible to check whether a property is accessible beforehand.
246
                 *
247
                 * @psalm-suppress MixedPropertyFetch
248
                 */
249 14
                return $array->$key;
250
            }
251
        }
252
253
        return $default;
254
    }
255
256
    /**
257
     * Retrieves the value of an array element or object property with the given key or property name.
258
     * If the key does not exist in the array or object, the default value will be returned instead.
259
     *
260
     * The key may be specified in a dot-separated format to retrieve the value of a sub-array or the property
261
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
262
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
263
     * or `$array->x` is neither an array nor an object, the default value will be returned.
264
     * Note that if the array already has an element `x.y.z`, then its value will be returned
265
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
266
     * like `['x', 'y', 'z']`.
267
     *
268
     * Below are some usage examples,
269
     *
270
     * ```php
271
     * // Using separated format to retrieve the property of embedded object:
272
     * $street = \Yiisoft\Arrays\ArrayHelper::getValue($users, 'address.street');
273
     *
274
     * // Using an array of keys to retrieve the value:
275
     * $value = \Yiisoft\Arrays\ArrayHelper::getValue($versions, ['1.0', 'date']);
276
     * ```
277
     *
278
     * @param array|object $array Array or object to extract value from.
279
     * @param array|Closure|float|int|string $path Key name of the array element, an array of keys or property name
280
     * of the object, or an anonymous function returning the value. The anonymous function signature should be:
281
     * `function($array, $defaultValue)`.
282
     * @param mixed $default The default value to be returned if the specified array key does not exist. Not used when
283
     * getting value from an object.
284
     * @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
285
     * to "." (dot).
286
     *
287
     * @psalm-param ArrayPath|Closure $path
288
     *
289
     * @return mixed The value of the element if found, default value otherwise.
290
     */
291 37
    public static function getValueByPath(
292
        array|object $array,
293
        array|Closure|float|int|string $path,
294
        mixed $default = null,
295
        string $delimiter = '.'
296
    ): mixed {
297 37
        return self::getValue(
298 37
            $array,
299 37
            $path instanceof Closure ? $path : self::parseMixedPath($path, $delimiter),
0 ignored issues
show
$path is never a sub-type of Closure.
Loading history...
300 37
            $default
301 37
        );
302
    }
303
304
    /**
305
     * Writes a value into an associative array at the key path specified.
306
     * If there is no such key path yet, it will be created recursively.
307
     * If the key exists, it will be overwritten.
308
     *
309
     * ```php
310
     *  $array = [
311
     *      'key' => [
312
     *          'in' => [
313
     *              'val1',
314
     *              'key' => 'val'
315
     *          ]
316
     *      ]
317
     *  ];
318
     * ```
319
     *
320
     * The result of `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
321
     * will be the following:
322
     *
323
     * ```php
324
     *  [
325
     *      'key' => [
326
     *          'in' => [
327
     *              'arr' => 'val'
328
     *          ]
329
     *      ]
330
     *  ]
331
     * ```
332
     *
333
     * @param array $array The array to write the value to.
334
     * @param array|float|int|string|null $key The path of where do you want to write a value to `$array`
335
     * the path can be described by an array of keys. If the path is null then `$array` will be assigned the `$value`.
336
     *
337
     * @psalm-param ArrayKey|null $key
338
     *
339
     * @param mixed $value The value to be written.
340
     */
341 29
    public static function setValue(array &$array, array|float|int|string|null $key, mixed $value): void
342
    {
343 29
        if ($key === null) {
0 ignored issues
show
The condition $key === null is always false.
Loading history...
344 2
            $array = $value;
345 2
            return;
346
        }
347
348 27
        $keys = is_array($key) ? $key : [$key];
0 ignored issues
show
The condition is_array($key) is always true.
Loading history...
349
350 27
        while (count($keys) > 1) {
351 15
            $k = self::normalizeArrayKey(array_shift($keys));
352 15
            if (!isset($array[$k])) {
353 8
                $array[$k] = [];
354
            }
355 15
            if (!is_array($array[$k])) {
356 2
                $array[$k] = [$array[$k]];
357
            }
358 15
            $array = &$array[$k];
359
            /** @var array $array */
360
        }
361
362 27
        $array[self::normalizeArrayKey(array_shift($keys))] = $value;
363
    }
364
365
    /**
366
     * Find array value in array at the key path specified and add passed value to him.
367
     *
368
     * If there is no such key path yet, it will be created recursively and an empty array will be initialized.
369
     *
370
     * ```php
371
     * $array = ['key' => []];
372
     *
373
     * ArrayHelper::addValue($array, ['key', 'in'], 'variable1');
374
     * ArrayHelper::addValue($array, ['key', 'in'], 'variable2');
375
     *
376
     * // Result: ['key' => ['in' => ['variable1', 'variable2']]]
377
     * ```
378
     *
379
     * If the value exists, it will become the first element of the array.
380
     *
381
     * ```php
382
     * $array = ['key' => 'in'];
383
     *
384
     * ArrayHelper::addValue($array, ['key'], 'variable1');
385
     *
386
     * // Result: ['key' => ['in', 'variable1']]
387
     * ```
388
     *
389
     * @param array $array The array to append the value to.
390
     * @param array|float|int|string|null $key The path of where do you want to append a value to `$array`. The path can
391
     * be described by an array of keys. If the path is null then `$value` will be appended to the `$array`.
392
     *
393
     * @psalm-param ArrayKey|null $key
394
     *
395
     * @param mixed $value The value to be appended.
396
     */
397 31
    public static function addValue(array &$array, array|float|int|string|null $key, mixed $value): void
398
    {
399 31
        if ($key === null) {
400 2
            $array[] = $value;
401 2
            return;
402
        }
403
404 29
        $keys = is_array($key) ? $key : [$key];
0 ignored issues
show
The condition is_array($key) is always true.
Loading history...
405
406 29
        while (count($keys) > 0) {
407 29
            $k = self::normalizeArrayKey(array_shift($keys));
408
409 29
            if (!array_key_exists($k, $array)) {
410 20
                $array[$k] = [];
411 14
            } elseif (!is_array($array[$k])) {
412 9
                $array[$k] = [$array[$k]];
413
            }
414
415 29
            $array = &$array[$k];
416
            /** @var array $array */
417
        }
418
419 29
        $array[] = $value;
420
    }
421
422
    /**
423
     * Find array value in array at the key path specified and add passed value to him.
424
     *
425
     * @see addValue
426
     *
427
     * @param array $array The array to append the value to.
428
     * @param array|float|int|string|null $path The path of where do you want to append a value to `$array`. The path
429
     * can be described by a string when each key should be separated by a dot. You can also describe the path as
430
     * an array of keys. If the path is null then `$value` will be appended to the `$array`.
431
     * @param mixed $value The value to be added.
432
     * @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
433
     * to "." (dot).
434
     *
435
     * @psalm-param ArrayPath|null $path
436
     */
437 20
    public static function addValueByPath(
438
        array &$array,
439
        array|float|int|string|null $path,
440
        mixed $value,
441
        string $delimiter = '.'
442
    ): void {
443 20
        self::addValue($array, $path === null ? null : self::parseMixedPath($path, $delimiter), $value);
0 ignored issues
show
The condition $path === null is always false.
Loading history...
444
    }
445
446
    /**
447
     * Writes a value into an associative array at the key path specified.
448
     * If there is no such key path yet, it will be created recursively.
449
     * If the key exists, it will be overwritten.
450
     *
451
     * ```php
452
     *  $array = [
453
     *      'key' => [
454
     *          'in' => [
455
     *              'val1',
456
     *              'key' => 'val'
457
     *          ]
458
     *      ]
459
     *  ];
460
     * ```
461
     *
462
     * The result of `ArrayHelper::setValue($array, 'key.in.0', ['arr' => 'val']);` will be the following:
463
     *
464
     * ```php
465
     *  [
466
     *      'key' => [
467
     *          'in' => [
468
     *              ['arr' => 'val'],
469
     *              'key' => 'val'
470
     *          ]
471
     *      ]
472
     *  ]
473
     *
474
     * ```
475
     *
476
     * The result of
477
     * `ArrayHelper::setValue($array, 'key.in', ['arr' => 'val']);` or
478
     * `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
479
     * will be the following:
480
     *
481
     * ```php
482
     *  [
483
     *      'key' => [
484
     *          'in' => [
485
     *              'arr' => 'val'
486
     *          ]
487
     *      ]
488
     *  ]
489
     * ```
490
     *
491
     * @param array $array The array to write the value to.
492
     * @param array|float|int|string|null $path The path of where do you want to write a value to `$array`.
493
     * The path can be described by a string when each key should be separated by a dot.
494
     * You can also describe the path as an array of keys. If the path is null then `$array` will be assigned
495
     * the `$value`.
496
     * @param mixed $value The value to be written.
497
     * @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
498
     * to "." (dot).
499
     *
500
     * @psalm-param ArrayPath|null $path
501
     */
502 21
    public static function setValueByPath(
503
        array &$array,
504
        array|float|int|string|null $path,
505
        mixed $value,
506
        string $delimiter = '.'
507
    ): void {
508 21
        self::setValue($array, $path === null ? null : self::parseMixedPath($path, $delimiter), $value);
0 ignored issues
show
The condition $path === null is always false.
Loading history...
509
    }
510
511
    /**
512
     * Removes an item from an array and returns the value. If the key does not exist in the array, the default value
513
     * will be returned instead.
514
     *
515
     * Usage examples,
516
     *
517
     * ```php
518
     * // $array = ['type' => 'A', 'options' => [1, 2]];
519
     *
520
     * // Working with array:
521
     * $type = \Yiisoft\Arrays\ArrayHelper::remove($array, 'type');
522
     *
523
     * // $array content
524
     * // $array = ['options' => [1, 2]];
525
     * ```
526
     *
527
     * @param array $array The array to extract value from.
528
     * @param array|float|int|string $key Key name of the array element or associative array at the key path specified.
529
     * @param mixed $default The default value to be returned if the specified key does not exist.
530
     *
531
     * @psalm-param ArrayKey $key
532
     *
533
     * @return mixed The value of the element if found, default value otherwise.
534
     */
535 13
    public static function remove(array &$array, array|float|int|string $key, mixed $default = null): mixed
536
    {
537 13
        $keys = is_array($key) ? $key : [$key];
0 ignored issues
show
The condition is_array($key) is always true.
Loading history...
538
539 13
        while (count($keys) > 1) {
540 7
            $key = self::normalizeArrayKey(array_shift($keys));
541 7
            if (!isset($array[$key]) || !is_array($array[$key])) {
542 1
                return $default;
543
            }
544 6
            $array = &$array[$key];
545
        }
546
547 12
        $key = self::normalizeArrayKey(array_shift($keys));
548 12
        if (array_key_exists($key, $array)) {
549 11
            $value = $array[$key];
550 11
            unset($array[$key]);
551 11
            return $value;
552
        }
553
554 1
        return $default;
555
    }
556
557
    /**
558
     * Removes an item from an array and returns the value. If the key does not exist in the array, the default value
559
     * will be returned instead.
560
     *
561
     * Usage examples,
562
     *
563
     * ```php
564
     * // $array = ['type' => 'A', 'options' => [1, 2]];
565
     *
566
     * // Working with array:
567
     * $type = \Yiisoft\Arrays\ArrayHelper::remove($array, 'type');
568
     *
569
     * // $array content
570
     * // $array = ['options' => [1, 2]];
571
     * ```
572
     *
573
     * @param array $array The array to extract value from.
574
     * @param array|float|int|string $path Key name of the array element or associative array at the key path specified.
575
     * The path can be described by a string when each key should be separated by a delimiter (default is dot).
576
     * @param mixed $default The default value to be returned if the specified key does not exist.
577
     * @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
578
     * to "." (dot).
579
     *
580
     * @psalm-param ArrayPath $path
581
     *
582
     * @return mixed The value of the element if found, default value otherwise.
583
     */
584 5
    public static function removeByPath(
585
        array &$array,
586
        array|float|int|string $path,
587
        mixed $default = null,
588
        string $delimiter = '.'
589
    ): mixed {
590 5
        return self::remove($array, self::parseMixedPath($path, $delimiter), $default);
591
    }
592
593
    /**
594
     * Removes items with matching values from the array and returns the removed items.
595
     *
596
     * Example,
597
     *
598
     * ```php
599
     * $array = ['Bob' => 'Dylan', 'Michael' => 'Jackson', 'Mick' => 'Jagger', 'Janet' => 'Jackson'];
600
     * $removed = \Yiisoft\Arrays\ArrayHelper::removeValue($array, 'Jackson');
601
     * // result:
602
     * // $array = ['Bob' => 'Dylan', 'Mick' => 'Jagger'];
603
     * // $removed = ['Michael' => 'Jackson', 'Janet' => 'Jackson'];
604
     * ```
605
     *
606
     * @param array $array The array where to look the value from.
607
     * @param mixed $value The value to remove from the array.
608
     *
609
     * @return array The items that were removed from the array.
610
     */
611 2
    public static function removeValue(array &$array, mixed $value): array
612
    {
613 2
        $result = [];
614 2
        foreach ($array as $key => $val) {
615 2
            if ($val === $value) {
616 1
                $result[$key] = $val;
617 1
                unset($array[$key]);
618
            }
619
        }
620
621 2
        return $result;
622
    }
623
624
    /**
625
     * Indexes and/or groups the array according to a specified key.
626
     * The input should be either multidimensional array or an array of objects.
627
     *
628
     * The `$key` can be either a key name of the sub-array, a property name of object, or an anonymous
629
     * function that must return the value that will be used as a key.
630
     *
631
     * `$groups` is an array of keys, that will be used to group the input array into one or more sub-arrays based
632
     * on keys specified.
633
     *
634
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
635
     * to `$groups` not specified then the element is discarded.
636
     *
637
     * For example:
638
     *
639
     * ```php
640
     * $array = [
641
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
642
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
643
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
644
     * ];
645
     * $result = ArrayHelper::index($array, 'id');
646
     * ```
647
     *
648
     * The result will be an associative array, where the key is the value of `id` attribute
649
     *
650
     * ```php
651
     * [
652
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
653
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
654
     *     // The second element of an original array is overwritten by the last element because of the same id
655
     * ]
656
     * ```
657
     *
658
     * An anonymous function can be used in the grouping array as well.
659
     *
660
     * ```php
661
     * $result = ArrayHelper::index($array, function ($element) {
662
     *     return $element['id'];
663
     * });
664
     * ```
665
     *
666
     * Passing `id` as a third argument will group `$array` by `id`:
667
     *
668
     * ```php
669
     * $result = ArrayHelper::index($array, null, 'id');
670
     * ```
671
     *
672
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
673
     * and indexed by `data` on the third level:
674
     *
675
     * ```php
676
     * [
677
     *     '123' => [
678
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
679
     *     ],
680
     *     '345' => [ // all elements with this index are present in the result array
681
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
682
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
683
     *     ]
684
     * ]
685
     * ```
686
     *
687
     * The anonymous function can be used in the array of grouping keys as well:
688
     *
689
     * ```php
690
     * $result = ArrayHelper::index($array, 'data', [function ($element) {
691
     *     return $element['id'];
692
     * }, 'device']);
693
     * ```
694
     *
695
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
696
     * and indexed by the `data` on the third level:
697
     *
698
     * ```php
699
     * [
700
     *     '123' => [
701
     *         'laptop' => [
702
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
703
     *         ]
704
     *     ],
705
     *     '345' => [
706
     *         'tablet' => [
707
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
708
     *         ],
709
     *         'smartphone' => [
710
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
711
     *         ]
712
     *     ]
713
     * ]
714
     * ```
715
     *
716
     * @param iterable $array The array or iterable object that needs to be indexed or grouped.
717
     * @param Closure|string|null $key The column name or anonymous function which result will be used
718
     * to index the array.
719
     * @param Closure[]|string|string[]|null $groups The array of keys, that will be used to group the input
720
     * array by one or more keys. If the `$key` attribute or its value for the particular element is null and `$groups`
721
     * is not defined, the array element will be discarded. Otherwise, if `$groups` is specified, array element will be
722
     * added to the result array without any key.
723
     *
724
     * @psalm-param iterable<mixed, array|object> $array
725
     *
726
     * @return array The indexed and/or grouped array.
727
     */
728 24
    public static function index(
729
        iterable $array,
730
        Closure|string|null $key,
731
        array|string|null $groups = []
732
    ): array {
733 24
        $result = [];
734 24
        $groups = (array)$groups;
735
736
        /** @var mixed $element */
737 24
        foreach ($array as $element) {
738 24
            if (!is_array($element) && !is_object($element)) {
739 8
                throw new InvalidArgumentException(
740 8
                    'index() can not get value from ' . gettype($element) .
741 8
                    '. The $array should be either multidimensional array or an array of objects.'
742 8
                );
743
            }
744
745 20
            $lastArray = &$result;
746
747 20
            foreach ($groups as $group) {
748 9
                $value = self::normalizeArrayKey(
749 9
                    self::getValue($element, $group)
750 9
                );
751 9
                if (!array_key_exists($value, $lastArray)) {
752 9
                    $lastArray[$value] = [];
753
                }
754
                /** @psalm-suppress MixedAssignment */
755 9
                $lastArray = &$lastArray[$value];
756
                /** @var array $lastArray */
757
            }
758
759 20
            if ($key === null) {
760 7
                if (!empty($groups)) {
761 7
                    $lastArray[] = $element;
762
                }
763
            } else {
764 13
                $value = self::getValue($element, $key);
765 13
                if ($value !== null) {
766 12
                    $lastArray[self::normalizeArrayKey($value)] = $element;
767
                }
768
            }
769 20
            unset($lastArray);
770
        }
771
772 16
        return $result;
773
    }
774
775
    /**
776
     * Groups the array according to a specified key.
777
     * This is just an alias for indexing by groups
778
     *
779
     * @param iterable $array The array or iterable object that needs to be grouped.
780
     * @param Closure[]|string|string[] $groups The array of keys, that will be used to group the input array
781
     * by one or more keys.
782
     *
783
     * @psalm-param iterable<mixed, array|object> $array
784
     *
785
     * @return array The grouped array.
786
     */
787 1
    public static function group(iterable $array, array|string $groups): array
788
    {
789 1
        return self::index($array, null, $groups);
790
    }
791
792
    /**
793
     * Returns the values of a specified column in an array.
794
     * The input array should be multidimensional or an array of objects.
795
     *
796
     * For example,
797
     *
798
     * ```php
799
     * $array = [
800
     *     ['id' => '123', 'data' => 'abc'],
801
     *     ['id' => '345', 'data' => 'def'],
802
     * ];
803
     * $result = ArrayHelper::getColumn($array, 'id');
804
     * // the result is: ['123', '345']
805
     *
806
     * // using anonymous function
807
     * $result = ArrayHelper::getColumn($array, function ($element) {
808
     *     return $element['id'];
809
     * });
810
     * ```
811
     *
812
     * @param iterable $array The array or iterable object to get column from.
813
     * @param Closure|string $name Column name or a closure returning column name.
814
     * @param bool $keepKeys Whether to maintain the array keys. If false, the resulting array
815
     * will be re-indexed with integers.
816
     *
817
     * @psalm-param iterable<array-key, array|object> $array
818
     *
819
     * @return array The list of column values.
820
     */
821 8
    public static function getColumn(iterable $array, Closure|string $name, bool $keepKeys = true): array
822
    {
823 8
        $result = [];
824 8
        if ($keepKeys) {
825 6
            foreach ($array as $k => $element) {
826 6
                $result[$k] = self::getValue($element, $name);
827
            }
828
        } else {
829 2
            foreach ($array as $element) {
830 2
                $result[] = self::getValue($element, $name);
831
            }
832
        }
833
834 8
        return $result;
835
    }
836
837
    /**
838
     * Builds a map (key-value pairs) from a multidimensional array or an array of objects.
839
     * The `$from` and `$to` parameters specify the key names or property names to set up the map.
840
     * Optionally, one can further group the map according to a grouping field `$group`.
841
     *
842
     * For example,
843
     *
844
     * ```php
845
     * $array = [
846
     *     ['id' => '123', 'name' => 'aaa', 'class' => 'x'],
847
     *     ['id' => '124', 'name' => 'bbb', 'class' => 'x'],
848
     *     ['id' => '345', 'name' => 'ccc', 'class' => 'y'],
849
     * ];
850
     *
851
     * $result = ArrayHelper::map($array, 'id', 'name');
852
     * // the result is:
853
     * // [
854
     * //     '123' => 'aaa',
855
     * //     '124' => 'bbb',
856
     * //     '345' => 'ccc',
857
     * // ]
858
     *
859
     * $result = ArrayHelper::map($array, 'id', 'name', 'class');
860
     * // the result is:
861
     * // [
862
     * //     'x' => [
863
     * //         '123' => 'aaa',
864
     * //         '124' => 'bbb',
865
     * //     ],
866
     * //     'y' => [
867
     * //         '345' => 'ccc',
868
     * //     ],
869
     * // ]
870
     * ```
871
     *
872
     * @param iterable $array Array or iterable object to build map from.
873
     * @param Closure|string $from Key or property name to map from.
874
     * @param Closure|string $to Key or property name to map to.
875
     * @param Closure|string|null $group Key or property to group the map.
876
     *
877
     * @psalm-param iterable<mixed, array|object> $array
878
     *
879
     * @return array Resulting map.
880
     */
881 9
    public static function map(
882
        iterable $array,
883
        Closure|string $from,
884
        Closure|string $to,
885
        Closure|string|null $group = null
886
    ): array {
887 9
        if ($group === null) {
888 4
            if ($from instanceof Closure || $to instanceof Closure || !is_array($array)) {
889 4
                $result = [];
890 4
                foreach ($array as $element) {
891 4
                    $key = (string)self::getValue($element, $from);
892 4
                    $result[$key] = self::getValue($element, $to);
893
                }
894
895 4
                return $result;
896
            }
897
898 2
            return array_column($array, $to, $from);
899
        }
900
901 5
        $result = [];
902 5
        foreach ($array as $element) {
903 5
            $groupKey = (string)self::getValue($element, $group);
904 5
            $key = (string)self::getValue($element, $from);
905 5
            $result[$groupKey][$key] = self::getValue($element, $to);
906
        }
907
908 5
        return $result;
909
    }
910
911
    /**
912
     * Checks if the given array contains the specified key.
913
     * This method enhances the `array_key_exists()` function by supporting case-insensitive
914
     * key comparison.
915
     *
916
     * @param array $array The array with keys to check.
917
     * @param array|float|int|string $key The key to check.
918
     * @param bool $caseSensitive Whether the key comparison should be case-sensitive.
919
     *
920
     * @psalm-param ArrayKey $key
921
     *
922
     * @return bool Whether the array contains the specified key.
923
     */
924 41
    public static function keyExists(array $array, array|float|int|string $key, bool $caseSensitive = true): bool
925
    {
926 41
        if (is_array($key)) {
0 ignored issues
show
The condition is_array($key) is always true.
Loading history...
927 31
            if (count($key) === 1) {
928 25
                return self::rootKeyExists($array, end($key), $caseSensitive);
929
            }
930
931 27
            foreach (self::getExistsKeys($array, array_shift($key), $caseSensitive) as $existKey) {
932 27
                $array = self::getRootValue($array, $existKey, null);
933 27
                if (is_array($array) && self::keyExists($array, $key, $caseSensitive)) {
934 14
                    return true;
935
                }
936
            }
937
938 13
            return false;
939
        }
940
941 10
        return self::rootKeyExists($array, $key, $caseSensitive);
942
    }
943
944 35
    private static function rootKeyExists(array $array, float|int|string $key, bool $caseSensitive): bool
945
    {
946 35
        $key = (string)$key;
947
948 35
        if ($caseSensitive) {
949 29
            return array_key_exists($key, $array);
950
        }
951
952 6
        foreach (array_keys($array) as $k) {
953 6
            if (strcasecmp($key, (string)$k) === 0) {
954 5
                return true;
955
            }
956
        }
957
958 1
        return false;
959
    }
960
961
    /**
962
     * @return array<int, array-key>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, array-key> at position 4 could not be parsed: Unknown type name 'array-key' at position 4 in array<int, array-key>.
Loading history...
963
     */
964 27
    private static function getExistsKeys(array $array, float|int|string $key, bool $caseSensitive): array
965
    {
966 27
        $key = (string)$key;
967
968 27
        if ($caseSensitive) {
969 22
            return [$key];
970
        }
971
972 5
        return array_filter(
973 5
            array_keys($array),
974 5
            static fn ($k) => strcasecmp($key, (string)$k) === 0
975 5
        );
976
    }
977
978
    /**
979
     * Checks if the given array contains the specified key. The key may be specified in a dot format.
980
     * In particular, if the key is `x.y.z`, then key would be `$array['x']['y']['z']`.
981
     *
982
     * This method enhances the `array_key_exists()` function by supporting case-insensitive
983
     * key comparison.
984
     *
985
     * @param array $array The array to check path in.
986
     * @param array|float|int|string $path The key path. Can be described by a string when each key should be separated
987
     * by delimiter. You can also describe the path as an array of keys.
988
     * @param bool $caseSensitive Whether the key comparison should be case-sensitive.
989
     * @param string $delimiter A separator, used to parse string $key for embedded object property retrieving. Defaults
990
     * to "." (dot).
991
     *
992
     * @psalm-param ArrayPath $path
993
     */
994 26
    public static function pathExists(
995
        array $array,
996
        array|float|int|string $path,
997
        bool $caseSensitive = true,
998
        string $delimiter = '.'
999
    ): bool {
1000 26
        return self::keyExists($array, self::parseMixedPath($path, $delimiter), $caseSensitive);
1001
    }
1002
1003
    /**
1004
     * Encodes special characters in an array of strings into HTML entities.
1005
     * Only array values will be encoded by default.
1006
     * If a value is an array, this method will also encode it recursively.
1007
     * Only string values will be encoded.
1008
     *
1009
     * @param iterable $data Data to be encoded.
1010
     * @param bool $valuesOnly Whether to encode array values only. If false,
1011
     * both the array keys and array values will be encoded.
1012
     * @param string|null $encoding The encoding to use, defaults to `ini_get('default_charset')`.
1013
     *
1014
     * @psalm-param iterable<mixed, mixed> $data
1015
     *
1016
     * @return array The encoded data.
1017
     *
1018
     * @link https://www.php.net/manual/en/function.htmlspecialchars.php
1019
     */
1020 2
    public static function htmlEncode(iterable $data, bool $valuesOnly = true, string $encoding = null): array
1021
    {
1022 2
        $d = [];
1023 2
        foreach ($data as $key => $value) {
1024 2
            if (!is_int($key)) {
1025 2
                $key = (string)$key;
1026
            }
1027 2
            if (!$valuesOnly && is_string($key)) {
1028 1
                $key = htmlspecialchars($key, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
1029
            }
1030 2
            if (is_string($value)) {
1031 2
                $d[$key] = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
1032 2
            } elseif (is_array($value)) {
1033 2
                $d[$key] = self::htmlEncode($value, $valuesOnly, $encoding);
1034
            } else {
1035 2
                $d[$key] = $value;
1036
            }
1037
        }
1038
1039 2
        return $d;
1040
    }
1041
1042
    /**
1043
     * Decodes HTML entities into the corresponding characters in an array of strings.
1044
     * Only array values will be decoded by default.
1045
     * If a value is an array, this method will also decode it recursively.
1046
     * Only string values will be decoded.
1047
     *
1048
     * @param iterable $data Data to be decoded.
1049
     * @param bool $valuesOnly Whether to decode array values only. If false,
1050
     * both the array keys and array values will be decoded.
1051
     *
1052
     * @psalm-param iterable<mixed, mixed> $data
1053
     *
1054
     * @return array The decoded data.
1055
     *
1056
     * @link https://www.php.net/manual/en/function.htmlspecialchars-decode.php
1057
     */
1058 2
    public static function htmlDecode(iterable $data, bool $valuesOnly = true): array
1059
    {
1060 2
        $decoded = [];
1061 2
        foreach ($data as $key => $value) {
1062 2
            if (!is_int($key)) {
1063 2
                $key = (string)$key;
1064
            }
1065 2
            if (!$valuesOnly && is_string($key)) {
1066 1
                $key = htmlspecialchars_decode($key, ENT_QUOTES);
1067
            }
1068 2
            if (is_string($value)) {
1069 2
                $decoded[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
1070 2
            } elseif (is_array($value)) {
1071 2
                $decoded[$key] = self::htmlDecode($value);
1072
            } else {
1073 2
                $decoded[$key] = $value;
1074
            }
1075
        }
1076
1077 2
        return $decoded;
1078
    }
1079
1080
    /**
1081
     * Returns a value indicating whether the given array is an associative array.
1082
     *
1083
     * An array is associative if all its keys are strings. If `$allStrings` is false,
1084
     * then an array will be treated as associative if at least one of its keys is a string.
1085
     *
1086
     * Note that an empty array will NOT be considered associative.
1087
     *
1088
     * @param array $array The array being checked.
1089
     * @param bool $allStrings Whether the array keys must be all strings in order for
1090
     * the array to be treated as associative.
1091
     *
1092
     * @return bool Whether the array is associative.
1093
     */
1094 1
    public static function isAssociative(array $array, bool $allStrings = true): bool
1095
    {
1096 1
        if ($array === []) {
1097 1
            return false;
1098
        }
1099
1100 1
        if ($allStrings) {
1101 1
            foreach ($array as $key => $_value) {
1102 1
                if (!is_string($key)) {
1103 1
                    return false;
1104
                }
1105
            }
1106
1107 1
            return true;
1108
        }
1109
1110 1
        foreach ($array as $key => $_value) {
1111 1
            if (is_string($key)) {
1112 1
                return true;
1113
            }
1114
        }
1115
1116 1
        return false;
1117
    }
1118
1119
    /**
1120
     * Returns a value indicating whether the given array is an indexed array.
1121
     *
1122
     * An array is indexed if all its keys are integers. If `$consecutive` is true,
1123
     * then the array keys must be a consecutive sequence starting from 0.
1124
     *
1125
     * Note that an empty array will be considered indexed.
1126
     *
1127
     * @param array $array The array being checked.
1128
     * @param bool $consecutive Whether the array keys must be a consecutive sequence
1129
     * in order for the array to be treated as indexed.
1130
     *
1131
     * @return bool Whether the array is indexed.
1132
     */
1133 1
    public static function isIndexed(array $array, bool $consecutive = false): bool
1134
    {
1135 1
        if ($array === []) {
1136 1
            return true;
1137
        }
1138
1139 1
        if ($consecutive) {
1140 1
            return array_keys($array) === range(0, count($array) - 1);
1141
        }
1142
1143 1
        foreach ($array as $key => $_value) {
1144 1
            if (!is_int($key)) {
1145 1
                return false;
1146
            }
1147
        }
1148
1149 1
        return true;
1150
    }
1151
1152
    /**
1153
     * Check whether an array or `\Traversable` contains an element.
1154
     *
1155
     * This method does the same as the PHP function {@see in_array()}
1156
     * but additionally works for objects that implement the {@see \Traversable} interface.
1157
     *
1158
     * @param mixed $needle The value to look for.
1159
     * @param iterable $haystack The set of values to search.
1160
     * @param bool $strict Whether to enable strict (`===`) comparison.
1161
     *
1162
     * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array.
1163
     *
1164
     * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise.
1165
     *
1166
     * @link https://php.net/manual/en/function.in-array.php
1167
     */
1168 3
    public static function isIn(mixed $needle, iterable $haystack, bool $strict = false): bool
1169
    {
1170 3
        if (is_array($haystack)) {
1171 3
            return in_array($needle, $haystack, $strict);
1172
        }
1173
1174 3
        foreach ($haystack as $value) {
1175 3
            if ($needle == $value && (!$strict || $needle === $value)) {
1176 3
                return true;
1177
            }
1178
        }
1179
1180 3
        return false;
1181
    }
1182
1183
    /**
1184
     * Checks whether an array or {@see \Traversable} is a subset of another array or {@see \Traversable}.
1185
     *
1186
     * This method will return `true`, if all elements of `$needles` are contained in
1187
     * `$haystack`. If at least one element is missing, `false` will be returned.
1188
     *
1189
     * @param iterable $needles The values that must **all** be in `$haystack`.
1190
     * @param iterable $haystack The set of value to search.
1191
     * @param bool $strict Whether to enable strict (`===`) comparison.
1192
     *
1193
     * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array.
1194
     *
1195
     * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise.
1196
     */
1197 1
    public static function isSubset(iterable $needles, iterable $haystack, bool $strict = false): bool
1198
    {
1199 1
        foreach ($needles as $needle) {
1200 1
            if (!self::isIn($needle, $haystack, $strict)) {
1201 1
                return false;
1202
            }
1203
        }
1204
1205 1
        return true;
1206
    }
1207
1208
    /**
1209
     * Filters array according to rules specified.
1210
     *
1211
     * For example:
1212
     *
1213
     * ```php
1214
     * $array = [
1215
     *     'A' => [1, 2],
1216
     *     'B' => [
1217
     *         'C' => 1,
1218
     *         'D' => 2,
1219
     *     ],
1220
     *     'E' => 1,
1221
     * ];
1222
     *
1223
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A']);
1224
     * // $result will be:
1225
     * // [
1226
     * //     'A' => [1, 2],
1227
     * // ]
1228
     *
1229
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A', 'B.C']);
1230
     * // $result will be:
1231
     * // [
1232
     * //     'A' => [1, 2],
1233
     * //     'B' => ['C' => 1],
1234
     * // ]
1235
     *
1236
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['B', '!B.C']);
1237
     * // $result will be:
1238
     * // [
1239
     * //     'B' => ['D' => 2],
1240
     * // ]
1241
     * ```
1242
     *
1243
     * @param array $array Source array.
1244
     * @param list<string> $filters Rules that define array keys which should be left or removed from results.
0 ignored issues
show
The type Yiisoft\Arrays\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1245
     * Each rule is:
1246
     * - `var` - `$array['var']` will be left in result.
1247
     * - `var.key` = only `$array['var']['key']` will be left in result.
1248
     * - `!var.key` = `$array['var']['key']` will be removed from result.
1249
     *
1250
     * @return array Filtered array.
1251
     */
1252 17
    public static function filter(array $array, array $filters): array
1253
    {
1254 17
        $result = [];
1255 17
        $excludeFilters = [];
1256
1257 17
        foreach ($filters as $filter) {
1258 17
            if ($filter[0] === '!') {
1259 6
                $excludeFilters[] = substr($filter, 1);
1260 6
                continue;
1261
            }
1262
1263 17
            $nodeValue = $array; // Set $array as root node.
1264 17
            $keys = explode('.', $filter);
1265 17
            foreach ($keys as $key) {
1266 17
                if (!is_array($nodeValue) || !array_key_exists($key, $nodeValue)) {
1267 4
                    continue 2; // Jump to next filter.
1268
                }
1269 15
                $nodeValue = $nodeValue[$key];
1270
            }
1271
1272
            // We've found a value now let's insert it.
1273 13
            $resultNode = &$result;
1274 13
            foreach ($keys as $key) {
1275 13
                if (!array_key_exists($key, $resultNode)) {
1276 13
                    $resultNode[$key] = [];
1277
                }
1278
                /** @psalm-suppress MixedAssignment */
1279 13
                $resultNode = &$resultNode[$key];
1280
                /** @var array $resultNode */
1281
            }
1282
            /** @var array */
1283 13
            $resultNode = $nodeValue;
1284
        }
1285
1286
        /**
1287
         * @psalm-suppress UnnecessaryVarAnnotation
1288
         *
1289
         * @var array $result
1290
         */
1291
1292 17
        foreach ($excludeFilters as $filter) {
1293 6
            $excludeNode = &$result;
1294 6
            $keys = explode('.', $filter);
1295 6
            $numNestedKeys = count($keys) - 1;
1296 6
            foreach ($keys as $i => $key) {
1297 6
                if (!is_array($excludeNode) || !array_key_exists($key, $excludeNode)) {
1298 2
                    continue 2; // Jump to next filter.
1299
                }
1300
1301 5
                if ($i < $numNestedKeys) {
1302
                    /** @psalm-suppress MixedAssignment */
1303 5
                    $excludeNode = &$excludeNode[$key];
1304
                } else {
1305 4
                    unset($excludeNode[$key]);
1306 4
                    break;
1307
                }
1308
            }
1309
        }
1310
1311
        /** @var array $result */
1312
1313 17
        return $result;
1314
    }
1315
1316
    /**
1317
     * Returns the public member variables of an object.
1318
     *
1319
     * This method is provided such that we can get the public member variables of an object, because a direct call of
1320
     * {@see get_object_vars()} (within the object itself) will return only private and protected variables.
1321
     *
1322
     * @param object $object The object to be handled.
1323
     *
1324
     * @return array The public member variables of the object.
1325
     *
1326
     * @link https://www.php.net/manual/en/function.get-object-vars.php
1327
     */
1328 4
    public static function getObjectVars(object $object): array
1329
    {
1330 4
        return get_object_vars($object);
1331
    }
1332
1333
    /**
1334
     * Rename key in array.
1335
     *
1336
     * @param array $array Source array.
1337
     * @param int|string $from Key to rename.
1338
     * @param int|string $to New key name.
1339
     *
1340
     * @return array The result array.
1341
     */
1342 5
    public static function renameKey(array $array, int|string $from, int|string $to): array
1343
    {
1344 5
        if (!isset($array[$from])) {
1345 2
            return $array;
1346
        }
1347
1348 3
        $keys = array_keys($array);
1349 3
        $index = array_search($from, $keys);
1350 3
        $keys[$index] = $to;
1351
1352 3
        return array_combine($keys, $array);
1353
    }
1354
1355
    /**
1356
     * @param array[] $arrays
1357
     */
1358 9
    private static function doMerge(array $arrays, ?int $depth, int $currentDepth = 0): array
1359
    {
1360 9
        $result = array_shift($arrays) ?: [];
1361 9
        while (!empty($arrays)) {
1362 8
            foreach (array_shift($arrays) as $key => $value) {
1363 8
                if (is_int($key)) {
1364 6
                    if (array_key_exists($key, $result)) {
1365 6
                        if ($result[$key] !== $value) {
1366 6
                            $result[] = $value;
1367
                        }
1368
                    } else {
1369 6
                        $result[$key] = $value;
1370
                    }
1371
                } elseif (
1372 6
                    isset($result[$key])
1373 6
                    && ($depth === null || $currentDepth < $depth)
1374 6
                    && is_array($value)
1375 6
                    && is_array($result[$key])
1376
                ) {
1377 5
                    $result[$key] = self::doMerge([$result[$key], $value], $depth, $currentDepth + 1);
1378
                } else {
1379 6
                    $result[$key] = $value;
1380
                }
1381
            }
1382
        }
1383 9
        return $result;
1384
    }
1385
1386 171
    private static function normalizeArrayKey(mixed $key): string
1387
    {
1388 171
        return is_float($key) ? NumericHelper::normalize($key) : (string)$key;
1389
    }
1390
1391
    /**
1392
     * @psalm-param ArrayPath $path
1393
     *
1394
     * @psalm-return ArrayKey
1395
     */
1396 105
    private static function parseMixedPath(array|float|int|string $path, string $delimiter): array|float|int|string
1397
    {
1398 105
        if (is_array($path)) {
0 ignored issues
show
The condition is_array($path) is always true.
Loading history...
1399 19
            $newPath = [];
1400 19
            foreach ($path as $key) {
1401 19
                if (is_string($key)) {
1402 19
                    $parsedPath = StringHelper::parsePath($key, $delimiter);
1403 19
                    $newPath = array_merge($newPath, $parsedPath);
1404 19
                    continue;
1405
                }
1406
1407 9
                if (is_array($key)) {
1408
                    /** @var list<float|int|string> $parsedPath */
1409 5
                    $parsedPath = self::parseMixedPath($key, $delimiter);
1410 5
                    $newPath = array_merge($newPath, $parsedPath);
0 ignored issues
show
$parsedPath of type Yiisoft\Arrays\list is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1410
                    $newPath = array_merge($newPath, /** @scrutinizer ignore-type */ $parsedPath);
Loading history...
1411 5
                    continue;
1412
                }
1413
1414 4
                $newPath[] = $key;
1415
            }
1416 19
            return $newPath;
1417
        }
1418
1419 87
        return is_string($path) ? StringHelper::parsePath($path, $delimiter) : $path;
1420
    }
1421
}
1422