Passed
Pull Request — master (#79)
by
unknown
02:25
created

ArrayHelper   F

Complexity

Total Complexity 149

Size/Duplication

Total Lines 1210
Duplicated Lines 0 %

Test Coverage

Coverage 98.57%

Importance

Changes 11
Bugs 0 Features 1
Metric Value
wmc 149
eloc 265
c 11
b 0
f 1
dl 0
loc 1210
ccs 276
cts 280
cp 0.9857
rs 2

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getValueByPath() 0 3 1
A merge() 0 11 3
A getRootValue() 0 18 5
A parsePath() 0 17 6
A setValueByPath() 0 3 1
A remove() 0 21 6
A removeByPath() 0 3 1
A removeValue() 0 13 3
A setValue() 0 23 6
A applyModifiers() 0 17 5
B performReverseBlockMerge() 0 24 11
B index() 0 38 9
C toArray() 0 55 15
A getColumn() 0 16 4
A getValue() 0 24 6
B performMerge() 0 24 10
B filter() 0 52 11
B isAssociative() 0 25 7
A isIn() 0 14 6
A getObjectVars() 0 3 1
A isIndexed() 0 18 5
A htmlDecode() 0 19 6
A keyExists() 0 13 4
A htmlEncode() 0 21 6
A map() 0 24 6
A normalizeArrayKey() 0 3 2
A isSubset() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like ArrayHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArrayHelper, and based on these observations, apply Extract Interface, too.

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\Arrays\Modifier\ModifierInterface;
11
use Yiisoft\Arrays\Modifier\ReverseBlockMerge;
12
use Yiisoft\Strings\NumericHelper;
13
14
/**
15
 * Yii array helper provides static methods allowing you to deal with arrays more efficiently.
16
 */
17
class ArrayHelper
18
{
19
    /**
20
     * Converts an object or an array of objects into an array.
21
     *
22
     * For example:
23
     *
24
     * ```php
25
     * [
26
     *     Post::class => [
27
     *         'id',
28
     *         'title',
29
     *         'createTime' => 'created_at',
30
     *         'length' => function ($post) {
31
     *             return strlen($post->content);
32
     *         },
33
     *     ],
34
     * ]
35
     * ```
36
     *
37
     * The result of `ArrayHelper::toArray($post, $properties)` could be like the following:
38
     *
39
     * ```php
40
     * [
41
     *     'id' => 123,
42
     *     'title' => 'test',
43
     *     'createTime' => '2013-01-01 12:00AM',
44
     *     'length' => 301,
45
     * ]
46
     * ```
47
     *
48
     * @param array|object|string $object the object to be converted into an array.
49
     *
50
     * It is possible to provide default way of converting object to array for a specific class by implementing
51
     * `Yiisoft\Arrays\ArrayableInterface` interface in that class.
52
     * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays.
53
     * The properties specified for each class is an array of the following format:
54
     *
55
     * - A field name to include as is.
56
     * - A key-value pair of desired array key name and model column name to take value from.
57
     * - A key-value pair of desired array key name and a callback which returns value.
58
     * @param bool $recursive whether to recursively converts properties which are objects into arrays.
59
     *
60
     * @return array the array representation of the object
61
     */
62 6
    public static function toArray($object, array $properties = [], bool $recursive = true): array
63
    {
64 6
        if (is_array($object)) {
65 5
            if ($recursive) {
66
                /** @var mixed $value */
67 4
                foreach ($object as $key => $value) {
68 4
                    if (is_array($value) || is_object($value)) {
69 4
                        $object[$key] = static::toArray($value, $properties, true);
70
                    }
71
                }
72
            }
73
74 5
            return $object;
75
        }
76
77 4
        if (is_object($object)) {
78 4
            if (!empty($properties)) {
79 1
                $className = get_class($object);
80 1
                if (!empty($properties[$className])) {
81 1
                    $result = [];
82
                    /**
83
                     * @var int|string $key
84
                     * @var string $name
85
                     */
86 1
                    foreach ($properties[$className] as $key => $name) {
87 1
                        if (is_int($key)) {
88
                            /** @var mixed */
89 1
                            $result[$name] = $object->$name;
90
                        } else {
91
                            /** @var mixed */
92 1
                            $result[$key] = static::getValue($object, $name);
93
                        }
94
                    }
95
96 1
                    return $recursive ? static::toArray($result, $properties) : $result;
97
                }
98
            }
99 4
            if ($object instanceof ArrayableInterface) {
100 3
                $result = $object->toArray([], [], $recursive);
101
            } else {
102 4
                $result = [];
103
                /**
104
                 * @var string $key
105
                 * @var mixed $value
106
                 */
107 4
                foreach ($object as $key => $value) {
108
                    /** @var mixed */
109 4
                    $result[$key] = $value;
110
                }
111
            }
112
113 4
            return $recursive ? static::toArray($result, $properties) : $result;
114
        }
115
116 1
        return [$object];
117
    }
118
119
    /**
120
     * Merges two or more arrays into one recursively.
121
     * If each array has an element with the same string key value, the latter
122
     * will overwrite the former (different from `array_merge_recursive`).
123
     * Recursive merging will be conducted if both arrays have an element of array
124
     * type and are having the same key.
125
     * For integer-keyed elements, the elements from the latter array will
126
     * be appended to the former array.
127
     * You can use modifiers {@see ArrayHelper::applyModifiers()} to change merging result.
128
     *
129
     * @param array ...$args arrays to be merged
130
     *
131
     * @return array the merged array (the original arrays are not changed)
132
     */
133 13
    public static function merge(...$args): array
134
    {
135 13
        $lastArray = end($args);
136
        if (
137 13
            isset($lastArray[ReverseBlockMerge::class]) &&
138 13
            $lastArray[ReverseBlockMerge::class] instanceof ReverseBlockMerge
139
        ) {
140 3
            return self::applyModifiers(self::performReverseBlockMerge(...$args));
141
        }
142
143 10
        return self::applyModifiers(self::performMerge(...$args));
144
    }
145
146 10
    private static function performMerge(array ...$args): array
147
    {
148 10
        $res = array_shift($args) ?: [];
149 10
        while (!empty($args)) {
150
            /** @psalm-var mixed $v */
151 9
            foreach (array_shift($args) as $k => $v) {
152 9
                if (is_int($k)) {
153 5
                    if (array_key_exists($k, $res) && $res[$k] !== $v) {
154
                        /** @var mixed */
155 3
                        $res[] = $v;
156
                    } else {
157
                        /** @var mixed */
158 5
                        $res[$k] = $v;
159
                    }
160 7
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
161 3
                    $res[$k] = self::performMerge($res[$k], $v);
162
                } else {
163
                    /** @var mixed */
164 7
                    $res[$k] = $v;
165
                }
166
            }
167
        }
168
169 10
        return $res;
170
    }
171
172 3
    private static function performReverseBlockMerge(array ...$args): array
173
    {
174 3
        $res = array_pop($args) ?: [];
175 3
        while (!empty($args)) {
176
            /** @psalm-var mixed $v */
177 3
            foreach (array_pop($args) as $k => $v) {
178 3
                if (is_int($k)) {
179 2
                    if (array_key_exists($k, $res) && $res[$k] !== $v) {
180
                        /** @var mixed */
181 2
                        $res[] = $v;
182
                    } else {
183
                        /** @var mixed */
184 2
                        $res[$k] = $v;
185
                    }
186 1
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
187 1
                    $res[$k] = self::performReverseBlockMerge($v, $res[$k]);
188 1
                } elseif (!isset($res[$k])) {
189
                    /** @var mixed */
190 1
                    $res[$k] = $v;
191
                }
192
            }
193
        }
194
195 3
        return $res;
196
    }
197
198
    /**
199
     * Apply modifiers (classes that implement {@link ModifierInterface}) in array.
200
     *
201
     * For example, {@link \Yiisoft\Arrays\Modifier\UnsetValue} to unset value from previous array or
202
     * {@link \Yiisoft\Arrays\ReplaceArrayValue} to force replace former value instead of recursive merging.
203
     *
204
     * @param array $data
205
     *
206
     * @return array
207
     *
208
     * @see ModifierInterface
209
     */
210 14
    public static function applyModifiers(array $data): array
211
    {
212 14
        $modifiers = [];
213
        /** @psalm-var mixed $v */
214 14
        foreach ($data as $k => $v) {
215 13
            if ($v instanceof ModifierInterface) {
216 9
                $modifiers[$k] = $v;
217 9
                unset($data[$k]);
218 13
            } elseif (is_array($v)) {
219 7
                $data[$k] = self::applyModifiers($v);
220
            }
221
        }
222 14
        ksort($modifiers);
223 14
        foreach ($modifiers as $key => $modifier) {
224 9
            $data = $modifier->apply($data, $key);
225
        }
226 14
        return $data;
227
    }
228
229
    /**
230
     * Retrieves the value of an array element or object property with the given key or property name.
231
     * If the key does not exist in the array or object, the default value will be returned instead.
232
     *
233
     * Below are some usage examples,
234
     *
235
     * ```php
236
     * // working with array
237
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($_POST, 'username');
238
     * // working with object
239
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($user, 'username');
240
     * // working with anonymous function
241
     * $fullName = \Yiisoft\Arrays\ArrayHelper::getValue($user, function ($user, $defaultValue) {
242
     *     return $user->firstName . ' ' . $user->lastName;
243
     * });
244
     * // using dot format to retrieve the property of embedded object
245
     * $street = \Yiisoft\Arrays\ArrayHelper::getValue($users, 'address.street');
246
     * // using an array of keys to retrieve the value
247
     * $value = \Yiisoft\Arrays\ArrayHelper::getValue($versions, ['1.0', 'date']);
248
     * ```
249
     *
250
     * @param array|object $array array or object to extract value from
251
     * @param array|Closure|float|int|string $key key name of the array element,
252
     * an array of keys or property name of the object, or an anonymous function
253
     * returning the value. The anonymous function signature should be:
254
     * `function($array, $defaultValue)`.
255
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
256
     * getting value from an object.
257
     *
258
     * @return mixed the value of the element if found, default value otherwise
259
     */
260 66
    public static function getValue($array, $key, $default = null)
261
    {
262 66
        if ($key instanceof Closure) {
263 8
            return $key($array, $default);
264
        }
265
266
        /** @psalm-suppress DocblockTypeContradiction */
267 63
        if (!is_array($array) && !is_object($array)) {
268 1
            throw new \InvalidArgumentException(
269 1
                'getValue() can not get value from ' . gettype($array) . '. Only array and object are supported.'
270
            );
271
        }
272
273 62
        if (is_array($key)) {
274
            /** @psalm-var array<mixed,string|int> $key */
275 37
            $lastKey = array_pop($key);
276 37
            foreach ($key as $keyPart) {
277
                /** @var mixed */
278 34
                $array = static::getRootValue($array, $keyPart, $default);
279
            }
280 37
            return static::getRootValue($array, $lastKey, $default);
281
        }
282
283 27
        return static::getRootValue($array, $key, $default);
284
    }
285
286
    /**
287
     * @param mixed $array array or object to extract value from, otherwise method will return $default
288
     * @param float|int|string $key key name of the array element or property name of the object,
289
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
290
     * getting value from an object.
291
     *
292
     * @return mixed the value of the element if found, default value otherwise
293
     */
294 62
    private static function getRootValue($array, $key, $default)
295
    {
296 62
        if (is_array($array)) {
297 50
            $key = static::normalizeArrayKey($key);
298 50
            return array_key_exists($key, $array) ? $array[$key] : $default;
299
        }
300
301 22
        if (is_object($array)) {
302
            try {
303 14
                return $array::$$key;
304 14
            } catch (Throwable $e) {
0 ignored issues
show
Unused Code introduced by
catch (\Throwable $e) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
305
                // this is expected to fail if the property does not exist, or __get() is not implemented
306
                // it is not reliably possible to check whether a property is accessible beforehand
307 14
                return $array->$key;
308
            }
309
        }
310
311 8
        return $default;
312
    }
313
314
    /**
315
     * Retrieves the value of an array element or object property with the given key or property name.
316
     * If the key does not exist in the array or object, the default value will be returned instead.
317
     *
318
     * The key may be specified in a dot format to retrieve the value of a sub-array or the property
319
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
320
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
321
     * or `$array->x` is neither an array nor an object, the default value will be returned.
322
     * Note that if the array already has an element `x.y.z`, then its value will be returned
323
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
324
     * like `['x', 'y', 'z']`.
325
     *
326
     * Below are some usage examples,
327
     *
328
     * ```php
329
     * // working with array
330
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($_POST, 'username');
331
     * // working with object
332
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($user, 'username');
333
     * // working with anonymous function
334
     * $fullName = \Yiisoft\Arrays\ArrayHelper::getValue($user, function ($user, $defaultValue) {
335
     *     return $user->firstName . ' ' . $user->lastName;
336
     * });
337
     * // using dot format to retrieve the property of embedded object
338
     * $street = \Yiisoft\Arrays\ArrayHelper::getValue($users, 'address.street');
339
     * // using an array of keys to retrieve the value
340
     * $value = \Yiisoft\Arrays\ArrayHelper::getValue($versions, ['1.0', 'date']);
341
     * ```
342
     *
343
     * @param array|object $array array or object to extract value from
344
     * @param array|Closure|float|int|string $path key name of the array element, an array of keys or property name
345
     * of the object, or an anonymous function returning the value. The anonymous function signature should be:
346
     * `function($array, $defaultValue)`.
347
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
348
     * getting value from an object.
349
     * @param string $delimiter
350
     *
351
     * @return mixed the value of the element if found, default value otherwise
352
     */
353 33
    public static function getValueByPath($array, $path, $default = null, string $delimiter = '.')
354
    {
355 33
        return static::getValue($array, static::parsePath($path, $delimiter), $default);
356
    }
357
358
    /**
359
     * Writes a value into an associative array at the key path specified.
360
     * If there is no such key path yet, it will be created recursively.
361
     * If the key exists, it will be overwritten.
362
     *
363
     * ```php
364
     *  $array = [
365
     *      'key' => [
366
     *          'in' => [
367
     *              'val1',
368
     *              'key' => 'val'
369
     *          ]
370
     *      ]
371
     *  ];
372
     * ```
373
     *
374
     * The result of `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
375
     * will be the following:
376
     *
377
     * ```php
378
     *  [
379
     *      'key' => [
380
     *          'in' => [
381
     *              'arr' => 'val'
382
     *          ]
383
     *      ]
384
     *  ]
385
     * ```
386
     *
387
     * @param array $array the array to write the value to
388
     * @param array|float|int|string|null $key the path of where do you want to write a value to `$array`
389
     * the path can be described by an array of keys
390
     * if the path is null then `$array` will be assigned the `$value`
391
     * @psalm-param array<mixed, string|int|float>|float|int|string|null $key
392
     *
393
     * @param mixed $value the value to be written
394
     */
395 29
    public static function setValue(array &$array, $key, $value): void
396
    {
397 29
        if ($key === null) {
398
            /** @var mixed */
399 2
            $array = $value;
400 2
            return;
401
        }
402
403 27
        $keys = is_array($key) ? $key : [$key];
404
405 27
        while (count($keys) > 1) {
406 15
            $k = static::normalizeArrayKey(array_shift($keys));
407 15
            if (!isset($array[$k])) {
408 8
                $array[$k] = [];
409
            }
410 15
            if (!is_array($array[$k])) {
411 2
                $array[$k] = [$array[$k]];
412
            }
413 15
            $array = &$array[$k];
414
        }
415
416
        /** @var mixed */
417 27
        $array[static::normalizeArrayKey(array_shift($keys))] = $value;
418 27
    }
419
420
    /**
421
     * Writes a value into an associative array at the key path specified.
422
     * If there is no such key path yet, it will be created recursively.
423
     * If the key exists, it will be overwritten.
424
     *
425
     * ```php
426
     *  $array = [
427
     *      'key' => [
428
     *          'in' => [
429
     *              'val1',
430
     *              'key' => 'val'
431
     *          ]
432
     *      ]
433
     *  ];
434
     * ```
435
     *
436
     * The result of `ArrayHelper::setValue($array, 'key.in.0', ['arr' => 'val']);` will be the following:
437
     *
438
     * ```php
439
     *  [
440
     *      'key' => [
441
     *          'in' => [
442
     *              ['arr' => 'val'],
443
     *              'key' => 'val'
444
     *          ]
445
     *      ]
446
     *  ]
447
     *
448
     * ```
449
     *
450
     * The result of
451
     * `ArrayHelper::setValue($array, 'key.in', ['arr' => 'val']);` or
452
     * `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
453
     * will be the following:
454
     *
455
     * ```php
456
     *  [
457
     *      'key' => [
458
     *          'in' => [
459
     *              'arr' => 'val'
460
     *          ]
461
     *      ]
462
     *  ]
463
     * ```
464
     *
465
     * @param array $array the array to write the value to
466
     * @param array|float|int|string|null $path the path of where do you want to write a value to `$array`
467
     * the path can be described by a string when each key should be separated by a dot
468
     * you can also describe the path as an array of keys
469
     * if the path is null then `$array` will be assigned the `$value`
470
     * @param mixed $value the value to be written
471
     * @param string $delimiter
472
     */
473 21
    public static function setValueByPath(array &$array, $path, $value, string $delimiter = '.'): void
474
    {
475 21
        static::setValue($array, static::parsePath($path, $delimiter), $value);
476 21
    }
477
478
    /**
479
     * @param mixed $path
480
     * @param string $delimiter
481
     *
482
     * @return mixed
483
     */
484 58
    private static function parsePath($path, string $delimiter)
485
    {
486 58
        if (is_string($path)) {
487 52
            return explode($delimiter, $path);
488
        }
489 18
        if (is_array($path)) {
490 12
            $newPath = [];
491 12
            foreach ($path as $key) {
492 12
                if (is_string($key) || is_array($key)) {
493 12
                    $newPath = array_merge($newPath, static::parsePath($key, $delimiter));
494
                } else {
495 2
                    $newPath[] = $key;
496
                }
497
            }
498 12
            return $newPath;
499
        }
500 6
        return $path;
501
    }
502
503
    /**
504
     * Removes an item from an array and returns the value. If the key does not exist in the array, the default value
505
     * will be returned instead.
506
     *
507
     * Usage examples,
508
     *
509
     * ```php
510
     * // $array = ['type' => 'A', 'options' => [1, 2]];
511
     * // working with array
512
     * $type = \Yiisoft\Arrays\ArrayHelper::remove($array, 'type');
513
     * // $array content
514
     * // $array = ['options' => [1, 2]];
515
     * ```
516
     *
517
     * @param array $array the array to extract value from
518
     * @param array|float|int|string $key key name of the array element or associative array at the key path specified
519
     * @psalm-param array<mixed, float|int|string>|float|int|string $key
520
     *
521
     * @param mixed $default the default value to be returned if the specified key does not exist
522
     *
523
     * @return mixed the value of the element if found, default value otherwise
524
     */
525 13
    public static function remove(array &$array, $key, $default = null)
526
    {
527 13
        $keys = is_array($key) ? $key : [$key];
528
529 13
        while (count($keys) > 1) {
530 7
            $key = static::normalizeArrayKey(array_shift($keys));
531 7
            if (!isset($array[$key]) || !is_array($array[$key])) {
532 1
                return $default;
533
            }
534 6
            $array = &$array[$key];
535
        }
536
537 12
        $key = static::normalizeArrayKey(array_shift($keys));
538 12
        if (array_key_exists($key, $array)) {
539
            /** @var mixed */
540 11
            $value = $array[$key];
541 11
            unset($array[$key]);
542 11
            return $value;
543
        }
544
545 1
        return $default;
546
    }
547
548
    /**
549
     * Removes an item from an array and returns the value. If the key does not exist in the array, the default value
550
     * will be returned instead.
551
     *
552
     * Usage examples,
553
     *
554
     * ```php
555
     * // $array = ['type' => 'A', 'options' => [1, 2]];
556
     * // working with array
557
     * $type = \Yiisoft\Arrays\ArrayHelper::remove($array, 'type');
558
     * // $array content
559
     * // $array = ['options' => [1, 2]];
560
     * ```
561
     *
562
     * @param array $array the array to extract value from
563
     * @param array|string $path key name of the array element or associative array at the key path specified
564
     * the path can be described by a string when each key should be separated by a delimiter (default is dot)
565
     * @param mixed $default the default value to be returned if the specified key does not exist
566
     * @param string $delimiter
567
     *
568
     * @return mixed the value of the element if found, default value otherwise
569
     */
570 5
    public static function removeByPath(array &$array, $path, $default = null, string $delimiter = '.')
571
    {
572 5
        return static::remove($array, static::parsePath($path, $delimiter), $default);
573
    }
574
575
    /**
576
     * Removes items with matching values from the array and returns the removed items.
577
     *
578
     * Example,
579
     *
580
     * ```php
581
     * $array = ['Bob' => 'Dylan', 'Michael' => 'Jackson', 'Mick' => 'Jagger', 'Janet' => 'Jackson'];
582
     * $removed = \Yiisoft\Arrays\ArrayHelper::removeValue($array, 'Jackson');
583
     * // result:
584
     * // $array = ['Bob' => 'Dylan', 'Mick' => 'Jagger'];
585
     * // $removed = ['Michael' => 'Jackson', 'Janet' => 'Jackson'];
586
     * ```
587
     *
588
     * @param array $array the array where to look the value from
589
     * @param mixed $value the value to remove from the array
590
     *
591
     * @return array the items that were removed from the array
592
     */
593 2
    public static function removeValue(array &$array, $value): array
594
    {
595 2
        $result = [];
596
        /** @psalm-var mixed $val */
597 2
        foreach ($array as $key => $val) {
598 2
            if ($val === $value) {
599
                /** @var mixed */
600 1
                $result[$key] = $val;
601 1
                unset($array[$key]);
602
            }
603
        }
604
605 2
        return $result;
606
    }
607
608
    /**
609
     * Indexes and/or groups the array according to a specified key.
610
     * The input should be either multidimensional array or an array of objects.
611
     *
612
     * The $key can be either a key name of the sub-array, a property name of object, or an anonymous
613
     * function that must return the value that will be used as a key.
614
     *
615
     * $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based
616
     * on keys specified.
617
     *
618
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
619
     * to `$groups` not specified then the element is discarded.
620
     *
621
     * For example:
622
     *
623
     * ```php
624
     * $array = [
625
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
626
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
627
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
628
     * ];
629
     * $result = ArrayHelper::index($array, 'id');
630
     * ```
631
     *
632
     * The result will be an associative array, where the key is the value of `id` attribute
633
     *
634
     * ```php
635
     * [
636
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
637
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
638
     *     // The second element of an original array is overwritten by the last element because of the same id
639
     * ]
640
     * ```
641
     *
642
     * An anonymous function can be used in the grouping array as well.
643
     *
644
     * ```php
645
     * $result = ArrayHelper::index($array, function ($element) {
646
     *     return $element['id'];
647
     * });
648
     * ```
649
     *
650
     * Passing `id` as a third argument will group `$array` by `id`:
651
     *
652
     * ```php
653
     * $result = ArrayHelper::index($array, null, 'id');
654
     * ```
655
     *
656
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
657
     * and indexed by `data` on the third level:
658
     *
659
     * ```php
660
     * [
661
     *     '123' => [
662
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
663
     *     ],
664
     *     '345' => [ // all elements with this index are present in the result array
665
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
666
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
667
     *     ]
668
     * ]
669
     * ```
670
     *
671
     * The anonymous function can be used in the array of grouping keys as well:
672
     *
673
     * ```php
674
     * $result = ArrayHelper::index($array, 'data', [function ($element) {
675
     *     return $element['id'];
676
     * }, 'device']);
677
     * ```
678
     *
679
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
680
     * and indexed by the `data` on the third level:
681
     *
682
     * ```php
683
     * [
684
     *     '123' => [
685
     *         'laptop' => [
686
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
687
     *         ]
688
     *     ],
689
     *     '345' => [
690
     *         'tablet' => [
691
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
692
     *         ],
693
     *         'smartphone' => [
694
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
695
     *         ]
696
     *     ]
697
     * ]
698
     * ```
699
     *
700
     * @param array $array the array that needs to be indexed or grouped
701
     * @psalm-param array<mixed, array|object> $array
702
     *
703
     * @param Closure|string|null $key the column name or anonymous function which result will be used to index the array
704
     * @param Closure[]|string|string[]|null $groups the array of keys, that will be used to group the input array
705
     * by one or more keys. If the $key attribute or its value for the particular element is null and $groups is not
706
     * defined, the array element will be discarded. Otherwise, if $groups is specified, array element will be added
707
     * to the result array without any key.
708
     *
709
     * @return array the indexed and/or grouped array
710
     */
711 7
    public static function index(array $array, $key, $groups = []): array
712
    {
713 7
        $result = [];
714 7
        $groups = (array)$groups;
715
716 7
        foreach ($array as $element) {
717
            /** @psalm-suppress DocblockTypeContradiction */
718 7
            if (!is_array($element) && !is_object($element)) {
719 4
                throw new \InvalidArgumentException(
720 4
                    'index() can not get value from ' . gettype($element)
721 4
                    . '. The $array should be either multidimensional array or an array of objects.'
722
                );
723
            }
724
725 5
            $lastArray = &$result;
726
727 5
            foreach ($groups as $group) {
728 1
                $value = static::getValue($element, $group);
729 1
                if (!array_key_exists($value, $lastArray)) {
730 1
                    $lastArray[$value] = [];
731
                }
732 1
                $lastArray = &$lastArray[$value];
733
            }
734
735 5
            if ($key === null) {
736 3
                if (!empty($groups)) {
737 3
                    $lastArray[] = $element;
738
                }
739
            } else {
740 4
                $value = static::getValue($element, $key);
741 4
                if ($value !== null) {
742 4
                    $lastArray[static::normalizeArrayKey($value)] = $element;
743
                }
744
            }
745 5
            unset($lastArray);
746
        }
747
748 3
        return $result;
749
    }
750
751
    /**
752
     * Returns the values of a specified column in an array.
753
     * The input array should be multidimensional or an array of objects.
754
     *
755
     * For example,
756
     *
757
     * ```php
758
     * $array = [
759
     *     ['id' => '123', 'data' => 'abc'],
760
     *     ['id' => '345', 'data' => 'def'],
761
     * ];
762
     * $result = ArrayHelper::getColumn($array, 'id');
763
     * // the result is: ['123', '345']
764
     *
765
     * // using anonymous function
766
     * $result = ArrayHelper::getColumn($array, function ($element) {
767
     *     return $element['id'];
768
     * });
769
     * ```
770
     *
771
     * @param array $array
772
     * @psalm-param array<mixed, array|object> $array
773
     *
774
     * @param Closure|string $name
775
     * @param bool $keepKeys whether to maintain the array keys. If false, the resulting array
776
     * will be re-indexed with integers.
777
     *
778
     * @return array the list of column values
779
     */
780 5
    public static function getColumn(array $array, $name, bool $keepKeys = true): array
781
    {
782 5
        $result = [];
783 5
        if ($keepKeys) {
784 5
            foreach ($array as $k => $element) {
785
                /** @var mixed */
786 5
                $result[$k] = static::getValue($element, $name);
787
            }
788
        } else {
789 1
            foreach ($array as $element) {
790
                /** @var mixed */
791 1
                $result[] = static::getValue($element, $name);
792
            }
793
        }
794
795 5
        return $result;
796
    }
797
798
    /**
799
     * Builds a map (key-value pairs) from a multidimensional array or an array of objects.
800
     * The `$from` and `$to` parameters specify the key names or property names to set up the map.
801
     * Optionally, one can further group the map according to a grouping field `$group`.
802
     *
803
     * For example,
804
     *
805
     * ```php
806
     * $array = [
807
     *     ['id' => '123', 'name' => 'aaa', 'class' => 'x'],
808
     *     ['id' => '124', 'name' => 'bbb', 'class' => 'x'],
809
     *     ['id' => '345', 'name' => 'ccc', 'class' => 'y'],
810
     * ];
811
     *
812
     * $result = ArrayHelper::map($array, 'id', 'name');
813
     * // the result is:
814
     * // [
815
     * //     '123' => 'aaa',
816
     * //     '124' => 'bbb',
817
     * //     '345' => 'ccc',
818
     * // ]
819
     *
820
     * $result = ArrayHelper::map($array, 'id', 'name', 'class');
821
     * // the result is:
822
     * // [
823
     * //     'x' => [
824
     * //         '123' => 'aaa',
825
     * //         '124' => 'bbb',
826
     * //     ],
827
     * //     'y' => [
828
     * //         '345' => 'ccc',
829
     * //     ],
830
     * // ]
831
     * ```
832
     *
833
     * @param array $array
834
     * @psalm-param array<mixed, array|object> $array
835
     *
836
     * @param Closure|string $from
837
     * @param Closure|string $to
838
     * @param Closure|string|null $group
839
     *
840
     * @return array
841
     */
842 1
    public static function map(array $array, $from, $to, $group = null): array
843
    {
844 1
        if ($group === null) {
845 1
            if($from instanceof Closure || $to instanceof Closure) {
846
                $result = [];
847
                foreach ($array as $element) {
848
                    /** @var mixed */
849
                    $result[static::getValue($element, $from)] = static::getValue($element, $to);
850
                }
851
852
                return $result;
853
            }
854
855 1
            return array_column($array, $to, $from);
856
        }
857
858 1
        $result = [];
859 1
        foreach ($array as $element) {
860 1
            $key = static::getValue($element, $from);
861
            /** @var mixed */
862 1
            $result[static::getValue($element, $group)][$key] = static::getValue($element, $to);
863
        }
864
865 1
        return $result;
866
    }
867
868
    /**
869
     * Checks if the given array contains the specified key.
870
     * This method enhances the `array_key_exists()` function by supporting case-insensitive
871
     * key comparison.
872
     *
873
     * @param array $array the array with keys to check
874
     * @param string $key the key to check
875
     * @param bool $caseSensitive whether the key comparison should be case-sensitive
876
     *
877
     * @return bool whether the array contains the specified key
878
     */
879 1
    public static function keyExists(array $array, string $key, bool $caseSensitive = true): bool
880
    {
881 1
        if ($caseSensitive) {
882 1
            return array_key_exists($key, $array);
883
        }
884
885 1
        foreach (array_keys($array) as $k) {
886 1
            if (strcasecmp($key, (string) $k) === 0) {
887 1
                return true;
888
            }
889
        }
890
891 1
        return false;
892
    }
893
894
    /**
895
     * Encodes special characters in an array of strings into HTML entities.
896
     * Only array values will be encoded by default.
897
     * If a value is an array, this method will also encode it recursively.
898
     * Only string values will be encoded.
899
     *
900
     * @param array $data data to be encoded
901
     * @psalm-param array<mixed, mixed> $data
902
     *
903
     * @param bool $valuesOnly whether to encode array values only. If false,
904
     * both the array keys and array values will be encoded.
905
     * @param string|null $encoding The encoding to use, defaults to `ini_get('default_charset')`.
906
     *
907
     * @return array the encoded data
908
     *
909
     * @see https://www.php.net/manual/en/function.htmlspecialchars.php
910
     */
911 1
    public static function htmlEncode(array $data, bool $valuesOnly = true, string $encoding = null): array
912
    {
913 1
        $d = [];
914
        /** @var mixed $value */
915 1
        foreach ($data as $key => $value) {
916 1
            if (!$valuesOnly && is_string($key)) {
917
                /** @psalm-suppress PossiblyNullArgument */
918 1
                $key = htmlspecialchars($key, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
919
            }
920 1
            if (is_string($value)) {
921
                /** @psalm-suppress PossiblyNullArgument */
922 1
                $d[$key] = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
923 1
            } elseif (is_array($value)) {
924 1
                $d[$key] = static::htmlEncode($value, $valuesOnly, $encoding);
925
            } else {
926
                /** @var mixed */
927 1
                $d[$key] = $value;
928
            }
929
        }
930
931 1
        return $d;
932
    }
933
934
    /**
935
     * Decodes HTML entities into the corresponding characters in an array of strings.
936
     * Only array values will be decoded by default.
937
     * If a value is an array, this method will also decode it recursively.
938
     * Only string values will be decoded.
939
     *
940
     * @param array $data data to be decoded
941
     * @psalm-param array<mixed, mixed> $data
942
     *
943
     * @param bool $valuesOnly whether to decode array values only. If false,
944
     * both the array keys and array values will be decoded.
945
     *
946
     * @return array the decoded data
947
     *
948
     * @see https://www.php.net/manual/en/function.htmlspecialchars-decode.php
949
     */
950 1
    public static function htmlDecode(array $data, bool $valuesOnly = true): array
951
    {
952 1
        $d = [];
953
        /** @psalm-var mixed $value */
954 1
        foreach ($data as $key => $value) {
955 1
            if (!$valuesOnly && is_string($key)) {
956 1
                $key = htmlspecialchars_decode($key, ENT_QUOTES);
957
            }
958 1
            if (is_string($value)) {
959 1
                $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
960 1
            } elseif (is_array($value)) {
961 1
                $d[$key] = static::htmlDecode($value);
962
            } else {
963
                /** @var mixed */
964 1
                $d[$key] = $value;
965
            }
966
        }
967
968 1
        return $d;
969
    }
970
971
    /**
972
     * Returns a value indicating whether the given array is an associative array.
973
     *
974
     * An array is associative if all its keys are strings. If `$allStrings` is false,
975
     * then an array will be treated as associative if at least one of its keys is a string.
976
     *
977
     * Note that an empty array will NOT be considered associative.
978
     *
979
     * @param array $array the array being checked
980
     * @param bool $allStrings whether the array keys must be all strings in order for
981
     * the array to be treated as associative.
982
     *
983
     * @return bool whether the array is associative
984
     */
985 1
    public static function isAssociative(array $array, bool $allStrings = true): bool
986
    {
987 1
        if ($array === []) {
988 1
            return false;
989
        }
990
991 1
        if ($allStrings) {
992
            /** @psalm-suppress MixedAssignment */
993 1
            foreach ($array as $key => $value) {
994 1
                if (!is_string($key)) {
995 1
                    return false;
996
                }
997
            }
998
999 1
            return true;
1000
        }
1001
1002
        /** @psalm-suppress MixedAssignment */
1003 1
        foreach ($array as $key => $value) {
1004 1
            if (is_string($key)) {
1005 1
                return true;
1006
            }
1007
        }
1008
1009 1
        return false;
1010
    }
1011
1012
    /**
1013
     * Returns a value indicating whether the given array is an indexed array.
1014
     *
1015
     * An array is indexed if all its keys are integers. If `$consecutive` is true,
1016
     * then the array keys must be a consecutive sequence starting from 0.
1017
     *
1018
     * Note that an empty array will be considered indexed.
1019
     *
1020
     * @param array $array the array being checked
1021
     * @param bool $consecutive whether the array keys must be a consecutive sequence
1022
     * in order for the array to be treated as indexed.
1023
     *
1024
     * @return bool whether the array is indexed
1025
     */
1026 1
    public static function isIndexed(array $array, bool $consecutive = false): bool
1027
    {
1028 1
        if ($array === []) {
1029 1
            return true;
1030
        }
1031
1032 1
        if ($consecutive) {
1033 1
            return array_keys($array) === range(0, count($array) - 1);
1034
        }
1035
1036
        /** @psalm-var mixed $value */
1037 1
        foreach ($array as $key => $value) {
1038 1
            if (!is_int($key)) {
1039 1
                return false;
1040
            }
1041
        }
1042
1043 1
        return true;
1044
    }
1045
1046
    /**
1047
     * Check whether an array or `\Traversable` contains an element.
1048
     *
1049
     * This method does the same as the PHP function [in_array()](https://php.net/manual/en/function.in-array.php)
1050
     * but additionally works for objects that implement the `\Traversable` interface.
1051
     *
1052
     * @param mixed $needle The value to look for.
1053
     * @param iterable $haystack The set of values to search.
1054
     * @param bool $strict Whether to enable strict (`===`) comparison.
1055
     *
1056
     * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array.
1057
     *
1058
     * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise.
1059
     *
1060
     * @see https://php.net/manual/en/function.in-array.php
1061
     */
1062 3
    public static function isIn($needle, iterable $haystack, bool $strict = false): bool
1063
    {
1064 3
        if (is_array($haystack)) {
1065 3
            return in_array($needle, $haystack, $strict);
1066
        }
1067
1068
        /** @psalm-var mixed $value */
1069 3
        foreach ($haystack as $value) {
1070 3
            if ($needle == $value && (!$strict || $needle === $value)) {
1071 3
                return true;
1072
            }
1073
        }
1074
1075 3
        return false;
1076
    }
1077
1078
    /**
1079
     * Checks whether an array or `\Traversable` is a subset of another array or `\Traversable`.
1080
     *
1081
     * This method will return `true`, if all elements of `$needles` are contained in
1082
     * `$haystack`. If at least one element is missing, `false` will be returned.
1083
     *
1084
     * @param iterable $needles The values that must **all** be in `$haystack`.
1085
     * @param iterable $haystack The set of value to search.
1086
     * @param bool $strict Whether to enable strict (`===`) comparison.
1087
     *
1088
     * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array.
1089
     *
1090
     * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise.
1091
     */
1092 1
    public static function isSubset(iterable $needles, iterable $haystack, bool $strict = false): bool
1093
    {
1094
        /** @psalm-var mixed $needle */
1095 1
        foreach ($needles as $needle) {
1096 1
            if (!static::isIn($needle, $haystack, $strict)) {
1097 1
                return false;
1098
            }
1099
        }
1100
1101 1
        return true;
1102
    }
1103
1104
    /**
1105
     * Filters array according to rules specified.
1106
     *
1107
     * For example:
1108
     *
1109
     * ```php
1110
     * $array = [
1111
     *     'A' => [1, 2],
1112
     *     'B' => [
1113
     *         'C' => 1,
1114
     *         'D' => 2,
1115
     *     ],
1116
     *     'E' => 1,
1117
     * ];
1118
     *
1119
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A']);
1120
     * // $result will be:
1121
     * // [
1122
     * //     'A' => [1, 2],
1123
     * // ]
1124
     *
1125
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A', 'B.C']);
1126
     * // $result will be:
1127
     * // [
1128
     * //     'A' => [1, 2],
1129
     * //     'B' => ['C' => 1],
1130
     * // ]
1131
     *
1132
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['B', '!B.C']);
1133
     * // $result will be:
1134
     * // [
1135
     * //     'B' => ['D' => 2],
1136
     * // ]
1137
     * ```
1138
     *
1139
     * @param array $array Source array
1140
     * @param array $filters Rules that define array keys which should be left or removed from results.
1141
     * Each rule is:
1142
     * - `var` - `$array['var']` will be left in result.
1143
     * - `var.key` = only `$array['var']['key']` will be left in result.
1144
     * - `!var.key` = `$array['var']['key']` will be removed from result.
1145
     *
1146
     * @return array Filtered array
1147
     */
1148 3
    public static function filter(array $array, array $filters): array
1149
    {
1150 3
        $result = [];
1151 3
        $excludeFilters = [];
1152
1153 3
        foreach ($filters as $filter) {
1154 3
            if ($filter[0] === '!') {
1155 1
                $excludeFilters[] = substr($filter, 1);
1156 1
                continue;
1157
            }
1158
1159 3
            $nodeValue = $array; //set $array as root node
1160 3
            $keys = explode('.', $filter);
1161 3
            foreach ($keys as $key) {
1162 3
                if (!array_key_exists($key, $nodeValue)) {
1163 1
                    continue 2; //Jump to next filter
1164
                }
1165 3
                $nodeValue = $nodeValue[$key];
1166
            }
1167
1168
            //We've found a value now let's insert it
1169 2
            $resultNode = &$result;
1170 2
            foreach ($keys as $key) {
1171 2
                if (!array_key_exists($key, $resultNode)) {
1172 2
                    $resultNode[$key] = [];
1173
                }
1174 2
                $resultNode = &$resultNode[$key];
1175
            }
1176 2
            $resultNode = $nodeValue;
1177
        }
1178
1179 3
        foreach ($excludeFilters as $filter) {
1180 1
            $excludeNode = &$result;
1181 1
            $keys = explode('.', $filter);
1182 1
            $numNestedKeys = count($keys) - 1;
1183 1
            foreach ($keys as $i => $key) {
1184 1
                if (!array_key_exists($key, $excludeNode)) {
1185 1
                    continue 2; //Jump to next filter
1186
                }
1187
1188 1
                if ($i < $numNestedKeys) {
1189
                    /** @psalm-suppress EmptyArrayAccess */
1190 1
                    $excludeNode = &$excludeNode[$key];
1191
                } else {
1192
                    /** @psalm-suppress EmptyArrayAccess */
1193 1
                    unset($excludeNode[$key]);
1194 1
                    break;
1195
                }
1196
            }
1197
        }
1198
1199 3
        return $result;
1200
    }
1201
1202
    /**
1203
     * Returns the public member variables of an object.
1204
     * This method is provided such that we can get the public member variables of an object.
1205
     * It is different from `get_object_vars()` because the latter will return private
1206
     * and protected variables if it is called within the object itself.
1207
     *
1208
     * @param object $object the object to be handled
1209
     *
1210
     * @return array|null the public member variables of the object or null if not object given
1211
     *
1212
     * @see https://www.php.net/manual/en/function.get-object-vars.php
1213
     */
1214 4
    public static function getObjectVars(object $object): ?array
1215
    {
1216 4
        return get_object_vars($object);
1217
    }
1218
1219
    /**
1220
     * @param float|int|string $key
1221
     *
1222
     * @return string
1223
     */
1224 89
    private static function normalizeArrayKey($key): string
1225
    {
1226 89
        return is_float($key) ? NumericHelper::normalize($key) : (string)$key;
1227
    }
1228
}
1229