Passed
Pull Request — master (#34)
by Alexander
26:50 queued 11:51
created

ArrayHelper::performReverseBlockMerge()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 11

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 20
rs 7.3166
cc 11
nc 7
nop 1
ccs 5
cts 5
cp 1
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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