Passed
Push — master ( 6d8250...ba51dd )
by Alexander
17:09 queued 16:02
created

ArrayHelper   F

Complexity

Total Complexity 136

Size/Duplication

Total Lines 948
Duplicated Lines 0 %

Test Coverage

Coverage 96.85%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 136
eloc 242
dl 0
loc 948
ccs 246
cts 254
cp 0.9685
rs 2
c 9
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
B filter() 0 50 11
B isAssociative() 0 23 7
A isIn() 0 13 6
B performReverseBlockMerge() 0 20 11
A isIndexed() 0 17 5
A htmlDecode() 0 17 6
C toArray() 0 43 15
A isSubset() 0 9 3
A merge() 0 9 3
B performMerge() 0 20 10
A isTraversable() 0 3 1
A setValue() 0 21 6
A applyModifiers() 0 16 5
B index() 0 33 8
A remove() 0 10 2
A keyExists() 0 13 4
A getColumn() 0 14 4
A map() 0 13 3
A removeValue() 0 11 3
C getValue() 0 49 16
A htmlEncode() 0 17 6
A getObjectVars() 0 3 1

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 InvalidArgumentException;
8
use Yiisoft\Arrays\Modifier\ModifierInterface;
9
use Yiisoft\Strings\StringHelper;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Strings\StringHelper 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...
10
use Yiisoft\Arrays\Modifier\ReverseBlockMerge;
11
12
/**
13
 * Yii array helper provides static methods allowing you to deal with arrays more efficiently.
14
 */
15
class ArrayHelper
16
{
17
    /**
18
     * Converts an object or an array of objects into an array.
19
     * @param object|array|string $object the object to be converted into an array
20
     * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays.
21
     * The properties specified for each class is an array of the following format:
22
     *
23
     * ```php
24
     * [
25
     *     'app\models\Post' => [
26
     *         'id',
27
     *         'title',
28
     *         // the key name in array result => property name
29
     *         'createTime' => 'created_at',
30
     *         // the key name in array result => anonymous function
31
     *         'length' => function ($post) {
32
     *             return strlen($post->content);
33
     *         },
34
     *     ],
35
     * ]
36
     * ```
37
     *
38
     * The result of `ArrayHelper::toArray($post, $properties)` could be like the following:
39
     *
40
     * ```php
41
     * [
42
     *     'id' => 123,
43
     *     'title' => 'test',
44
     *     'createTime' => '2013-01-01 12:00AM',
45
     *     'length' => 301,
46
     * ]
47
     * ```
48
     *
49
     * @param bool $recursive whether to recursively converts properties which are objects into arrays.
50
     * @return array the array representation of the object
51
     */
52 2
    public static function toArray($object, array $properties = [], bool $recursive = true): array
53
    {
54 2
        if (is_array($object)) {
55 2
            if ($recursive) {
56 2
                foreach ($object as $key => $value) {
57 2
                    if (is_array($value) || is_object($value)) {
58 2
                        $object[$key] = static::toArray($value, $properties, true);
59
                    }
60
                }
61
            }
62
63 2
            return $object;
64
        }
65
66 1
        if (is_object($object)) {
67 1
            if (!empty($properties)) {
68 1
                $className = get_class($object);
69 1
                if (!empty($properties[$className])) {
70 1
                    $result = [];
71 1
                    foreach ($properties[$className] as $key => $name) {
72 1
                        if (is_int($key)) {
73 1
                            $result[$name] = $object->$name;
74
                        } else {
75 1
                            $result[$key] = static::getValue($object, $name);
76
                        }
77
                    }
78
79 1
                    return $recursive ? static::toArray($result, $properties) : $result;
80
                }
81
            }
82 1
            if ($object instanceof ArrayableInterface) {
83
                $result = $object->toArray([], [], $recursive);
84
            } else {
85 1
                $result = [];
86 1
                foreach ($object as $key => $value) {
87 1
                    $result[$key] = $value;
88
                }
89
            }
90
91 1
            return $recursive ? static::toArray($result, $properties) : $result;
92
        }
93
94 1
        return [$object];
95
    }
96
97
    /**
98
     * Merges two or more arrays into one recursively.
99
     * If each array has an element with the same string key value, the latter
100
     * will overwrite the former (different from array_merge_recursive).
101
     * Recursive merging will be conducted if both arrays have an element of array
102
     * type and are having the same key.
103
     * For integer-keyed elements, the elements from the latter array will
104
     * be appended to the former array.
105
     * You can use modifiers to change merging result.
106
     * @param array $args arrays to be merged
107
     * @return array the merged array (the original arrays are not changed.)
108
     */
109 10
    public static function merge(...$args): array
110
    {
111 10
        $lastArray = end($args);
112 10
        if (isset($lastArray[ReverseBlockMerge::class]) && $lastArray[ReverseBlockMerge::class] instanceof ReverseBlockMerge) {
113 1
            reset($args);
114 1
            return self::applyModifiers(self::performReverseBlockMerge(...$args));
115
        }
116
117 9
        return self::applyModifiers(self::performMerge(...$args));
118
    }
119
120 9
    private static function performMerge(...$args): array
121
    {
122 9
        $res = array_shift($args) ?: [];
123 9
        while (!empty($args)) {
124 8
            foreach (array_shift($args) as $k => $v) {
125 8
                if (is_int($k)) {
126 5
                    if (array_key_exists($k, $res) && $res[$k] !== $v) {
127 3
                        $res[] = $v;
128
                    } else {
129 5
                        $res[$k] = $v;
130
                    }
131 6
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
132 3
                    $res[$k] = self::performMerge($res[$k], $v);
133
                } else {
134 6
                    $res[$k] = $v;
135
                }
136
            }
137
        }
138
139 9
        return $res;
140
    }
141
142 1
    private static function performReverseBlockMerge(...$args): array
143
    {
144 1
        $res = array_pop($args) ?: [];
145 1
        while (!empty($args)) {
146 1
            foreach (array_pop($args) as $k => $v) {
147 1
                if (is_int($k)) {
148
                    if (array_key_exists($k, $res) && $res[$k] !== $v) {
149
                        $res[] = $v;
150
                    } else {
151
                        $res[$k] = $v;
152
                    }
153 1
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
154 1
                    $res[$k] = self::performReverseBlockMerge($v, $res[$k]);
155 1
                } elseif (!isset($res[$k])) {
156 1
                    $res[$k] = $v;
157
                }
158
            }
159
        }
160
161 1
        return $res;
162
    }
163
164 10
    public static function applyModifiers(array $data): array
165
    {
166 10
        $modifiers = [];
167 10
        foreach ($data as $k => $v) {
168 9
            if ($v instanceof ModifierInterface) {
169 6
                $modifiers[$k] = $v;
170 6
                unset($data[$k]);
171 9
            } elseif (is_array($v)) {
172 7
                $data[$k] = self::applyModifiers($v);
173
            }
174
        }
175 10
        ksort($modifiers);
176 10
        foreach ($modifiers as $key => $modifier) {
177 6
            $data = $modifier->apply($data, $key);
178
        }
179 10
        return $data;
180
    }
181
182
    /**
183
     * Retrieves the value of an array element or object property with the given key or property name.
184
     * If the key does not exist in the array or object, the default value will be returned instead.
185
     *
186
     * The key may be specified in a dot format to retrieve the value of a sub-array or the property
187
     * of an embedded object. In particular, if the key is `x.y.z`, then the returned value would
188
     * be `$array['x']['y']['z']` or `$array->x->y->z` (if `$array` is an object). If `$array['x']`
189
     * or `$array->x` is neither an array nor an object, the default value will be returned.
190
     * Note that if the array already has an element `x.y.z`, then its value will be returned
191
     * instead of going through the sub-arrays. So it is better to be done specifying an array of key names
192
     * like `['x', 'y', 'z']`.
193
     *
194
     * Below are some usage examples,
195
     *
196
     * ```php
197
     * // working with array
198
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($_POST, 'username');
199
     * // working with object
200
     * $username = \Yiisoft\Arrays\ArrayHelper::getValue($user, 'username');
201
     * // working with anonymous function
202
     * $fullName = \Yiisoft\Arrays\ArrayHelper::getValue($user, function ($user, $defaultValue) {
203
     *     return $user->firstName . ' ' . $user->lastName;
204
     * });
205
     * // using dot format to retrieve the property of embedded object
206
     * $street = \Yiisoft\Arrays\ArrayHelper::getValue($users, 'address.street');
207
     * // using an array of keys to retrieve the value
208
     * $value = \Yiisoft\Arrays\ArrayHelper::getValue($versions, ['1.0', 'date']);
209
     * ```
210
     *
211
     * @param array|object $array array or object to extract value from
212
     * @param string|\Closure|array $key key name of the array element, an array of keys or property name of the object,
213
     * or an anonymous function returning the value. The anonymous function signature should be:
214
     * `function($array, $defaultValue)`.
215
     * @param mixed $default the default value to be returned if the specified array key does not exist. Not used when
216
     * getting value from an object.
217
     * @return mixed the value of the element if found, default value otherwise
218
     */
219 39
    public static function getValue($array, $key, $default = null)
220
    {
221 39
        if ($key instanceof \Closure) {
222 7
            return $key($array, $default);
223
        }
224
225 37
        if (!is_array($array) && !is_object($array)) {
226
            throw new \InvalidArgumentException(
227
                'getValue() can not get value from ' . gettype($array) . '. Only array and object are supported.'
228
            );
229
        }
230
231 37
        if (is_array($key)) {
232 2
            $lastKey = array_pop($key);
233 2
            foreach ($key as $keyPart) {
234 2
                $array = static::getValue($array, $keyPart, $default);
235
            }
236 2
            return static::getValue($array, $lastKey, $default);
237
        }
238
239 37
        if (is_array($array) && array_key_exists((string)$key, $array)) {
240 14
            return $array[$key];
241
        }
242
243 24
        if (strpos($key, '.') !== false) {
244 14
            foreach (explode('.', $key) as $part) {
245 14
                if (is_array($array)) {
246 11
                    if (!array_key_exists($part, $array)) {
247 5
                        return $default;
248
                    }
249 9
                    $array = $array[$part];
250 5
                } elseif (is_object($array)) {
251 5
                    if (!property_exists($array, $part) && empty($array)) {
252
                        return $default;
253
                    }
254 5
                    $array = $array->$part;
255
                }
256
            }
257
258 9
            return $array;
259
        }
260
261 10
        if (is_object($array)) {
262
            // this is expected to fail if the property does not exist, or __get() is not implemented
263
            // it is not reliably possible to check whether a property is accessible beforehand
264 7
            return $array->$key;
265
        }
266
267 3
        return $default;
268
    }
269
270
    /**
271
     * Writes a value into an associative array at the key path specified.
272
     * If there is no such key path yet, it will be created recursively.
273
     * If the key exists, it will be overwritten.
274
     *
275
     * ```php
276
     *  $array = [
277
     *      'key' => [
278
     *          'in' => [
279
     *              'val1',
280
     *              'key' => 'val'
281
     *          ]
282
     *      ]
283
     *  ];
284
     * ```
285
     *
286
     * The result of `ArrayHelper::setValue($array, 'key.in.0', ['arr' => 'val']);` will be the following:
287
     *
288
     * ```php
289
     *  [
290
     *      'key' => [
291
     *          'in' => [
292
     *              ['arr' => 'val'],
293
     *              'key' => 'val'
294
     *          ]
295
     *      ]
296
     *  ]
297
     *
298
     * ```
299
     *
300
     * The result of
301
     * `ArrayHelper::setValue($array, 'key.in', ['arr' => 'val']);` or
302
     * `ArrayHelper::setValue($array, ['key', 'in'], ['arr' => 'val']);`
303
     * will be the following:
304
     *
305
     * ```php
306
     *  [
307
     *      'key' => [
308
     *          'in' => [
309
     *              'arr' => 'val'
310
     *          ]
311
     *      ]
312
     *  ]
313
     * ```
314
     *
315
     * @param array $array the array to write the value to
316
     * @param string|array|null $path the path of where do you want to write a value to `$array`
317
     * the path can be described by a string when each key should be separated by a dot
318
     * you can also describe the path as an array of keys
319
     * if the path is null then `$array` will be assigned the `$value`
320
     * @param mixed $value the value to be written
321
     */
322 16
    public static function setValue(array &$array, $path, $value): void
323
    {
324 16
        if ($path === null) {
325 1
            $array = $value;
326 1
            return;
327
        }
328
329 15
        $keys = is_array($path) ? $path : explode('.', $path);
330
331 15
        while (count($keys) > 1) {
332 12
            $key = array_shift($keys);
333 12
            if (!isset($array[$key])) {
334 5
                $array[$key] = [];
335
            }
336 12
            if (!is_array($array[$key])) {
337 2
                $array[$key] = [$array[$key]];
338
            }
339 12
            $array = &$array[$key];
340
        }
341
342 15
        $array[array_shift($keys)] = $value;
343 15
    }
344
345
    /**
346
     * Removes an item from an array and returns the value. If the key does not exist in the array, the default value
347
     * will be returned instead.
348
     *
349
     * Usage examples,
350
     *
351
     * ```php
352
     * // $array = ['type' => 'A', 'options' => [1, 2]];
353
     * // working with array
354
     * $type = \Yiisoft\Arrays\ArrayHelper::remove($array, 'type');
355
     * // $array content
356
     * // $array = ['options' => [1, 2]];
357
     * ```
358
     *
359
     * @param array $array the array to extract value from
360
     * @param string $key key name of the array element
361
     * @param mixed $default the default value to be returned if the specified key does not exist
362
     * @return mixed the value of the element if found, default value otherwise
363
     */
364 1
    public static function remove(array &$array, string $key, $default = null)
365
    {
366 1
        if (array_key_exists($key, $array)) {
367 1
            $value = $array[$key];
368 1
            unset($array[$key]);
369
370 1
            return $value;
371
        }
372
373 1
        return $default;
374
    }
375
376
    /**
377
     * Removes items with matching values from the array and returns the removed items.
378
     *
379
     * Example,
380
     *
381
     * ```php
382
     * $array = ['Bob' => 'Dylan', 'Michael' => 'Jackson', 'Mick' => 'Jagger', 'Janet' => 'Jackson'];
383
     * $removed = \Yiisoft\Arrays\ArrayHelper::removeValue($array, 'Jackson');
384
     * // result:
385
     * // $array = ['Bob' => 'Dylan', 'Mick' => 'Jagger'];
386
     * // $removed = ['Michael' => 'Jackson', 'Janet' => 'Jackson'];
387
     * ```
388
     *
389
     * @param array $array the array where to look the value from
390
     * @param mixed $value the value to remove from the array
391
     * @return array the items that were removed from the array
392
     */
393 2
    public static function removeValue(array &$array, $value): array
394
    {
395 2
        $result = [];
396 2
        foreach ($array as $key => $val) {
397 2
            if ($val === $value) {
398 1
                $result[$key] = $val;
399 1
                unset($array[$key]);
400
            }
401
        }
402
403 2
        return $result;
404
    }
405
406
    /**
407
     * Indexes and/or groups the array according to a specified key.
408
     * The input should be either multidimensional array or an array of objects.
409
     *
410
     * The $key can be either a key name of the sub-array, a property name of object, or an anonymous
411
     * function that must return the value that will be used as a key.
412
     *
413
     * $groups is an array of keys, that will be used to group the input array into one or more sub-arrays based
414
     * on keys specified.
415
     *
416
     * If the `$key` is specified as `null` or a value of an element corresponding to the key is `null` in addition
417
     * to `$groups` not specified then the element is discarded.
418
     *
419
     * For example:
420
     *
421
     * ```php
422
     * $array = [
423
     *     ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
424
     *     ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
425
     *     ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
426
     * ];
427
     * $result = ArrayHelper::index($array, 'id');
428
     * ```
429
     *
430
     * The result will be an associative array, where the key is the value of `id` attribute
431
     *
432
     * ```php
433
     * [
434
     *     '123' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop'],
435
     *     '345' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
436
     *     // The second element of an original array is overwritten by the last element because of the same id
437
     * ]
438
     * ```
439
     *
440
     * An anonymous function can be used in the grouping array as well.
441
     *
442
     * ```php
443
     * $result = ArrayHelper::index($array, function ($element) {
444
     *     return $element['id'];
445
     * });
446
     * ```
447
     *
448
     * Passing `id` as a third argument will group `$array` by `id`:
449
     *
450
     * ```php
451
     * $result = ArrayHelper::index($array, null, 'id');
452
     * ```
453
     *
454
     * The result will be a multidimensional array grouped by `id` on the first level, by `device` on the second level
455
     * and indexed by `data` on the third level:
456
     *
457
     * ```php
458
     * [
459
     *     '123' => [
460
     *         ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
461
     *     ],
462
     *     '345' => [ // all elements with this index are present in the result array
463
     *         ['id' => '345', 'data' => 'def', 'device' => 'tablet'],
464
     *         ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'],
465
     *     ]
466
     * ]
467
     * ```
468
     *
469
     * The anonymous function can be used in the array of grouping keys as well:
470
     *
471
     * ```php
472
     * $result = ArrayHelper::index($array, 'data', [function ($element) {
473
     *     return $element['id'];
474
     * }, 'device']);
475
     * ```
476
     *
477
     * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on the second one
478
     * and indexed by the `data` on the third level:
479
     *
480
     * ```php
481
     * [
482
     *     '123' => [
483
     *         'laptop' => [
484
     *             'abc' => ['id' => '123', 'data' => 'abc', 'device' => 'laptop']
485
     *         ]
486
     *     ],
487
     *     '345' => [
488
     *         'tablet' => [
489
     *             'def' => ['id' => '345', 'data' => 'def', 'device' => 'tablet']
490
     *         ],
491
     *         'smartphone' => [
492
     *             'hgi' => ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone']
493
     *         ]
494
     *     ]
495
     * ]
496
     * ```
497
     *
498
     * @param array $array the array that needs to be indexed or grouped
499
     * @param string|\Closure|null $key the column name or anonymous function which result will be used to index the array
500
     * @param string|string[]|\Closure[]|null $groups the array of keys, that will be used to group the input array
501
     * by one or more keys. If the $key attribute or its value for the particular element is null and $groups is not
502
     * defined, the array element will be discarded. Otherwise, if $groups is specified, array element will be added
503
     * to the result array without any key.
504
     * @return array the indexed and/or grouped array
505
     */
506 3
    public static function index(array $array, $key, $groups = []): array
507
    {
508 3
        $result = [];
509 3
        $groups = (array)$groups;
510
511 3
        foreach ($array as $element) {
512 3
            $lastArray = &$result;
513
514 3
            foreach ($groups as $group) {
515 1
                $value = static::getValue($element, $group);
516 1
                if (!array_key_exists($value, $lastArray)) {
517 1
                    $lastArray[$value] = [];
518
                }
519 1
                $lastArray = &$lastArray[$value];
520
            }
521
522 3
            if ($key === null) {
523 2
                if (!empty($groups)) {
524 2
                    $lastArray[] = $element;
525
                }
526
            } else {
527 3
                $value = static::getValue($element, $key);
528 3
                if ($value !== null) {
529 3
                    if (is_float($value)) {
530 1
                        $value = str_replace(',', '.', (string) $value);
531
                    }
532 3
                    $lastArray[$value] = $element;
533
                }
534
            }
535 3
            unset($lastArray);
536
        }
537
538 3
        return $result;
539
    }
540
541
    /**
542
     * Returns the values of a specified column in an array.
543
     * The input array should be multidimensional or an array of objects.
544
     *
545
     * For example,
546
     *
547
     * ```php
548
     * $array = [
549
     *     ['id' => '123', 'data' => 'abc'],
550
     *     ['id' => '345', 'data' => 'def'],
551
     * ];
552
     * $result = ArrayHelper::getColumn($array, 'id');
553
     * // the result is: ['123', '345']
554
     *
555
     * // using anonymous function
556
     * $result = ArrayHelper::getColumn($array, function ($element) {
557
     *     return $element['id'];
558
     * });
559
     * ```
560
     *
561
     * @param array $array
562
     * @param string|\Closure $name
563
     * @param bool $keepKeys whether to maintain the array keys. If false, the resulting array
564
     * will be re-indexed with integers.
565
     * @return array the list of column values
566
     */
567 5
    public static function getColumn(array $array, $name, bool $keepKeys = true): array
568
    {
569 5
        $result = [];
570 5
        if ($keepKeys) {
571 5
            foreach ($array as $k => $element) {
572 5
                $result[$k] = static::getValue($element, $name);
573
            }
574
        } else {
575 1
            foreach ($array as $element) {
576 1
                $result[] = static::getValue($element, $name);
577
            }
578
        }
579
580 5
        return $result;
581
    }
582
583
    /**
584
     * Builds a map (key-value pairs) from a multidimensional array or an array of objects.
585
     * The `$from` and `$to` parameters specify the key names or property names to set up the map.
586
     * Optionally, one can further group the map according to a grouping field `$group`.
587
     *
588
     * For example,
589
     *
590
     * ```php
591
     * $array = [
592
     *     ['id' => '123', 'name' => 'aaa', 'class' => 'x'],
593
     *     ['id' => '124', 'name' => 'bbb', 'class' => 'x'],
594
     *     ['id' => '345', 'name' => 'ccc', 'class' => 'y'],
595
     * ];
596
     *
597
     * $result = ArrayHelper::map($array, 'id', 'name');
598
     * // the result is:
599
     * // [
600
     * //     '123' => 'aaa',
601
     * //     '124' => 'bbb',
602
     * //     '345' => 'ccc',
603
     * // ]
604
     *
605
     * $result = ArrayHelper::map($array, 'id', 'name', 'class');
606
     * // the result is:
607
     * // [
608
     * //     'x' => [
609
     * //         '123' => 'aaa',
610
     * //         '124' => 'bbb',
611
     * //     ],
612
     * //     'y' => [
613
     * //         '345' => 'ccc',
614
     * //     ],
615
     * // ]
616
     * ```
617
     *
618
     * @param array $array
619
     * @param string|\Closure $from
620
     * @param string|\Closure $to
621
     * @param string|\Closure $group
622
     * @return array
623
     */
624 1
    public static function map(array $array, $from, $to, $group = null): array
625
    {
626 1
        if ($group === null) {
627 1
            return array_column($array, $to, $from);
628
        }
629
630 1
        $result = [];
631 1
        foreach ($array as $element) {
632 1
            $key = static::getValue($element, $from);
633 1
            $result[static::getValue($element, $group)][$key] = static::getValue($element, $to);
634
        }
635
636 1
        return $result;
637
    }
638
639
    /**
640
     * Checks if the given array contains the specified key.
641
     * This method enhances the `array_key_exists()` function by supporting case-insensitive
642
     * key comparison.
643
     * @param array $array the array with keys to check
644
     * @param string $key the key to check
645
     * @param bool $caseSensitive whether the key comparison should be case-sensitive
646
     * @return bool whether the array contains the specified key
647
     */
648 1
    public static function keyExists(array $array, string $key, bool $caseSensitive = true): bool
649
    {
650 1
        if ($caseSensitive) {
651 1
            return array_key_exists($key, $array);
652
        }
653
654 1
        foreach (array_keys($array) as $k) {
655 1
            if (strcasecmp($key, $k) === 0) {
656 1
                return true;
657
            }
658
        }
659
660 1
        return false;
661
    }
662
663
    /**
664
     * Encodes special characters in an array of strings into HTML entities.
665
     * Only array values will be encoded by default.
666
     * If a value is an array, this method will also encode it recursively.
667
     * Only string values will be encoded.
668
     * @param array $data data to be encoded
669
     * @param bool $valuesOnly whether to encode array values only. If false,
670
     * both the array keys and array values will be encoded.
671
     * @param string|null $encoding The encoding to use, defaults to `ini_get('default_charset')`.
672
     * @return array the encoded data
673
     * @see http://www.php.net/manual/en/function.htmlspecialchars.php
674
     */
675 1
    public static function htmlEncode(array $data, bool $valuesOnly = true, string $encoding = null): array
676
    {
677 1
        $d = [];
678 1
        foreach ($data as $key => $value) {
679 1
            if (!$valuesOnly && is_string($key)) {
680 1
                $key = htmlspecialchars($key, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
681
            }
682 1
            if (is_string($value)) {
683 1
                $d[$key] = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, $encoding, true);
684 1
            } elseif (is_array($value)) {
685 1
                $d[$key] = static::htmlEncode($value, $valuesOnly, $encoding);
686
            } else {
687 1
                $d[$key] = $value;
688
            }
689
        }
690
691 1
        return $d;
692
    }
693
694
    /**
695
     * Decodes HTML entities into the corresponding characters in an array of strings.
696
     * Only array values will be decoded by default.
697
     * If a value is an array, this method will also decode it recursively.
698
     * Only string values will be decoded.
699
     * @param array $data data to be decoded
700
     * @param bool $valuesOnly whether to decode array values only. If false,
701
     * both the array keys and array values will be decoded.
702
     * @return array the decoded data
703
     * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
704
     */
705 1
    public static function htmlDecode(array $data, bool $valuesOnly = true): array
706
    {
707 1
        $d = [];
708 1
        foreach ($data as $key => $value) {
709 1
            if (!$valuesOnly && is_string($key)) {
710 1
                $key = htmlspecialchars_decode($key, ENT_QUOTES);
711
            }
712 1
            if (is_string($value)) {
713 1
                $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES);
714 1
            } elseif (is_array($value)) {
715 1
                $d[$key] = static::htmlDecode($value);
716
            } else {
717 1
                $d[$key] = $value;
718
            }
719
        }
720
721 1
        return $d;
722
    }
723
724
    /**
725
     * Returns a value indicating whether the given array is an associative array.
726
     *
727
     * An array is associative if all its keys are strings. If `$allStrings` is false,
728
     * then an array will be treated as associative if at least one of its keys is a string.
729
     *
730
     * Note that an empty array will NOT be considered associative.
731
     *
732
     * @param array $array the array being checked
733
     * @param bool $allStrings whether the array keys must be all strings in order for
734
     * the array to be treated as associative.
735
     * @return bool whether the array is associative
736
     */
737 1
    public static function isAssociative(array $array, bool $allStrings = true): bool
738
    {
739 1
        if ($array === []) {
740 1
            return false;
741
        }
742
743 1
        if ($allStrings) {
744 1
            foreach ($array as $key => $value) {
745 1
                if (!is_string($key)) {
746 1
                    return false;
747
                }
748
            }
749
750 1
            return true;
751
        }
752
753 1
        foreach ($array as $key => $value) {
754 1
            if (is_string($key)) {
755 1
                return true;
756
            }
757
        }
758
759 1
        return false;
760
    }
761
762
    /**
763
     * Returns a value indicating whether the given array is an indexed array.
764
     *
765
     * An array is indexed if all its keys are integers. If `$consecutive` is true,
766
     * then the array keys must be a consecutive sequence starting from 0.
767
     *
768
     * Note that an empty array will be considered indexed.
769
     *
770
     * @param array $array the array being checked
771
     * @param bool $consecutive whether the array keys must be a consecutive sequence
772
     * in order for the array to be treated as indexed.
773
     * @return bool whether the array is indexed
774
     */
775 1
    public static function isIndexed(array $array, bool $consecutive = false): bool
776
    {
777 1
        if ($array === []) {
778 1
            return true;
779
        }
780
781 1
        if ($consecutive) {
782 1
            return array_keys($array) === range(0, count($array) - 1);
783
        }
784
785 1
        foreach ($array as $key => $value) {
786 1
            if (!is_int($key)) {
787 1
                return false;
788
            }
789
        }
790
791 1
        return true;
792
    }
793
794
    /**
795
     * Check whether an array or [[\Traversable]] contains an element.
796
     *
797
     * This method does the same as the PHP function [in_array()](http://php.net/manual/en/function.in-array.php)
798
     * but additionally works for objects that implement the [[\Traversable]] interface.
799
     * @param mixed $needle The value to look for.
800
     * @param iterable $haystack The set of values to search.
801
     * @param bool $strict Whether to enable strict (`===`) comparison.
802
     * @return bool `true` if `$needle` was found in `$haystack`, `false` otherwise.
803
     * @throws InvalidArgumentException if `$haystack` is neither traversable nor an array.
804
     * @see http://php.net/manual/en/function.in-array.php
805
     */
806 3
    public static function isIn($needle, iterable $haystack, bool $strict = false): bool
807
    {
808 3
        if (is_array($haystack)) {
809 3
            return in_array($needle, $haystack, $strict);
810
        }
811
812 3
        foreach ($haystack as $value) {
813 3
            if ($needle == $value && (!$strict || $needle === $value)) {
814 3
                return true;
815
            }
816
        }
817
818 3
        return false;
819
    }
820
821
    /**
822
     * Checks whether a variable is an array or [[\Traversable]].
823
     *
824
     * This method does the same as the PHP function [is_array()](http://php.net/manual/en/function.is-array.php)
825
     * but additionally works on objects that implement the [[\Traversable]] interface.
826
     * @param mixed $var The variable being evaluated.
827
     * @return bool whether $var is array-like
828
     * @see http://php.net/manual/en/function.is-array.php
829
     */
830 1
    public static function isTraversable($var): bool
831
    {
832 1
        return is_iterable($var);
833
    }
834
835
    /**
836
     * Checks whether an array or [[\Traversable]] is a subset of another array or [[\Traversable]].
837
     *
838
     * This method will return `true`, if all elements of `$needles` are contained in
839
     * `$haystack`. If at least one element is missing, `false` will be returned.
840
     * @param iterable $needles The values that must **all** be in `$haystack`.
841
     * @param iterable $haystack The set of value to search.
842
     * @param bool $strict Whether to enable strict (`===`) comparison.
843
     * @return bool `true` if `$needles` is a subset of `$haystack`, `false` otherwise.
844
     * @throws InvalidArgumentException if `$haystack` or `$needles` is neither traversable nor an array.
845
     */
846 1
    public static function isSubset(iterable $needles, iterable $haystack, bool $strict = false): bool
847
    {
848 1
        foreach ($needles as $needle) {
849 1
            if (!static::isIn($needle, $haystack, $strict)) {
850 1
                return false;
851
            }
852
        }
853
854 1
        return true;
855
    }
856
857
    /**
858
     * Filters array according to rules specified.
859
     *
860
     * For example:
861
     *
862
     * ```php
863
     * $array = [
864
     *     'A' => [1, 2],
865
     *     'B' => [
866
     *         'C' => 1,
867
     *         'D' => 2,
868
     *     ],
869
     *     'E' => 1,
870
     * ];
871
     *
872
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A']);
873
     * // $result will be:
874
     * // [
875
     * //     'A' => [1, 2],
876
     * // ]
877
     *
878
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['A', 'B.C']);
879
     * // $result will be:
880
     * // [
881
     * //     'A' => [1, 2],
882
     * //     'B' => ['C' => 1],
883
     * // ]
884
     *
885
     * $result = \Yiisoft\Arrays\ArrayHelper::filter($array, ['B', '!B.C']);
886
     * // $result will be:
887
     * // [
888
     * //     'B' => ['D' => 2],
889
     * // ]
890
     * ```
891
     *
892
     * @param array $array Source array
893
     * @param array $filters Rules that define array keys which should be left or removed from results.
894
     * Each rule is:
895
     * - `var` - `$array['var']` will be left in result.
896
     * - `var.key` = only `$array['var']['key'] will be left in result.
897
     * - `!var.key` = `$array['var']['key'] will be removed from result.
898
     * @return array Filtered array
899
     */
900 3
    public static function filter(array $array, array $filters): array
901
    {
902 3
        $result = [];
903 3
        $excludeFilters = [];
904
905 3
        foreach ($filters as $filter) {
906 3
            if ($filter[0] === '!') {
907 1
                $excludeFilters[] = substr($filter, 1);
908 1
                continue;
909
            }
910
911 3
            $nodeValue = $array; //set $array as root node
912 3
            $keys = explode('.', $filter);
913 3
            foreach ($keys as $key) {
914 3
                if (!array_key_exists($key, $nodeValue)) {
915 1
                    continue 2; //Jump to next filter
916
                }
917 3
                $nodeValue = $nodeValue[$key];
918
            }
919
920
            //We've found a value now let's insert it
921 2
            $resultNode = &$result;
922 2
            foreach ($keys as $key) {
923 2
                if (!array_key_exists($key, $resultNode)) {
924 2
                    $resultNode[$key] = [];
925
                }
926 2
                $resultNode = &$resultNode[$key];
927
            }
928 2
            $resultNode = $nodeValue;
929
        }
930
931 3
        foreach ($excludeFilters as $filter) {
932 1
            $excludeNode = &$result;
933 1
            $keys = explode('.', $filter);
934 1
            $numNestedKeys = count($keys) - 1;
935 1
            foreach ($keys as $i => $key) {
936 1
                if (!array_key_exists($key, $excludeNode)) {
937
                    continue 2; //Jump to next filter
938
                }
939
940 1
                if ($i < $numNestedKeys) {
941 1
                    $excludeNode = &$excludeNode[$key];
942
                } else {
943 1
                    unset($excludeNode[$key]);
944 1
                    break;
945
                }
946
            }
947
        }
948
949 3
        return $result;
950
    }
951
952
    /**
953
     * Returns the public member variables of an object.
954
     * This method is provided such that we can get the public member variables of an object.
955
     * It is different from "get_object_vars()" because the latter will return private
956
     * and protected variables if it is called within the object itself.
957
     * @param object $object the object to be handled
958
     * @return array|null the public member variables of the object or null if not object given
959
     */
960 2
    public static function getObjectVars(object $object): ?array
961
    {
962 2
        return get_object_vars($object);
963
    }
964
}
965