Passed
Push — master ( 955315...1a74af )
by Alexander
27:27 queued 26:19
created

ArrayHelper   F

Complexity

Total Complexity 136

Size/Duplication

Total Lines 948
Duplicated Lines 0 %

Test Coverage

Coverage 99.61%

Importance

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

22 Methods

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