Arr::first()   A
last analyzed

Complexity

Conditions 6
Paths 8

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 9
c 0
b 0
f 0
nc 8
nop 3
dl 0
loc 22
rs 9.2222
1
<?php
2
3
/**
4
 * Platine Stdlib
5
 *
6
 * Platine Stdlib is a the collection of frequently used php features
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Stdlib
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Arr.php
33
 *
34
 *  The Array helper class
35
 *
36
 *  @package    Platine\Stdlib\Helper
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   https://www.platine-php.com
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
45
declare(strict_types=1);
46
47
namespace Platine\Stdlib\Helper;
48
49
use ArrayAccess;
50
use Closure;
51
use InvalidArgumentException;
52
use Platine\Stdlib\Contract\Arrayable;
53
use Stringable;
54
use Traversable;
55
56
57
/**
58
 * @class Arr
59
 * @package Platine\Stdlib\Helper
60
 */
61
class Arr
62
{
63
    /**
64
     * Convert an array, object or string to array
65
     * @param array<mixed>|object|string $object
66
     * @param array<string, array<int|string, string>> $properties
67
     * @param bool $recursive
68
     * @return array<mixed>
69
     */
70
    public static function toArray(
71
        array|object|string $object,
72
        array $properties = [],
73
        bool $recursive = true
74
    ): array {
75
        if (is_array($object)) {
0 ignored issues
show
introduced by
The condition is_array($object) is always true.
Loading history...
76
            if ($recursive) {
77
                foreach ($object as $key => $value) {
78
                    if (is_array($value) || is_object($value)) {
79
                        $object[$key] = static::toArray($value, $properties, true);
80
                    }
81
                }
82
            }
83
84
            return $object;
85
        }
86
87
        if (is_object($object)) {
88
            if (count($properties) > 0) {
89
                $className = get_class($object);
90
                if (count($properties[$className]) > 0) {
91
                    $result = [];
92
                    foreach ($properties[$className] as $key => $name) {
93
                        if (is_int($key)) {
94
                            $result[$name] = $object->{$name};
95
                        } else {
96
                            $result[$key] = static::getValue($object, $name);
97
                        }
98
                    }
99
100
                    return $recursive
101
                            ? static::toArray($result, $properties)
102
                            : $result;
103
                }
104
            }
105
106
            if ($object instanceof Arrayable) {
107
                $result = $object->toArray();
108
            } else {
109
                $result = [];
110
                foreach ((array)$object as $key => $value) {
111
                    $result[$key] = $value;
112
                }
113
            }
114
115
            return $recursive
116
                    ? static::toArray($result, $properties)
117
                    : $result;
118
        }
119
120
        return [$object];
121
    }
122
123
    /**
124
     * Merge the passed arrays
125
     * @param array<mixed> ...$args
126
     * @return array<mixed>
127
     */
128
    public static function merge(array ...$args): array
129
    {
130
        $res = (array)array_shift($args);
131
        while (count($args) > 0) {
132
            $next = array_shift($args);
133
            foreach ($next as $key => $value) {
134
                if (is_int($key)) {
135
                    if (isset($res[$key])) {
136
                        $res[] = $value;
137
                    } else {
138
                        $res[$key] = $value;
139
                    }
140
                } elseif (is_array($value) && isset($res[$key]) && is_array($res[$key])) {
141
                    $res[$key] = self::merge($res[$key], $value);
142
                } else {
143
                    $res[$key] = $value;
144
                }
145
            }
146
        }
147
148
        return $res;
149
    }
150
151
    /**
152
     * Return the value of an array element or object property
153
     * for the given key or property name.
154
     *
155
     * @param mixed $object
156
     * @param int|string|Closure|array<mixed> $key
157
     * @param mixed $default
158
     *
159
     * @return mixed If the key does not exist in the array or object,
160
     * the default value will be returned instead.
161
     */
162
    public static function getValue(
163
        mixed $object,
164
        int|string|Closure|array $key,
165
        mixed $default = null
166
    ): mixed {
167
        if ($key instanceof Closure) {
0 ignored issues
show
introduced by
$key is never a sub-type of Closure.
Loading history...
168
            return $key($object, $default);
169
        }
170
171
        if (is_array($key)) {
0 ignored issues
show
introduced by
The condition is_array($key) is always true.
Loading history...
172
            $lastKey = array_pop($key);
173
            foreach ($key as $part) {
174
                $object = static::getValue($object, $part);
175
            }
176
            $key = $lastKey;
177
        }
178
179
        if (
180
            is_array($object) && (isset($object[$key])
181
                || array_key_exists($key, $object))
182
        ) {
183
            return $object[$key];
184
        }
185
186
        if (is_string($key)) {
187
            $pos = strpos($key, '.');
188
189
            if ($pos !== false) {
190
                $object = static::getValue(
191
                    $object,
192
                    substr($key, 0, $pos),
193
                    $default
194
                );
195
                $key = (string) substr($key, $pos + 1);
196
            }
197
        }
198
199
        // Note: property_exists not detected property with magic
200
        // Method so add isset for this purpose
201
        if (is_object($object) && (property_exists($object, $key) || isset($object->{$key}))) {
202
            // this is will fail if the property does not exist,
203
            //  or __get() is not implemented
204
            // it is not reliably possible to check whether a property
205
            // is accessable beforehand
206
207
            return $object->{$key};
208
        }
209
210
        if (is_array($object)) {
211
            return (isset($object[$key]) || array_key_exists($key, $object))
212
                    ? $object[$key]
213
                    : $default;
214
        }
215
216
        return $default;
217
    }
218
219
    /**
220
     * Remove an item from an array and returns the value.
221
     * If the key does not exist in the array, the default value will be returned
222
     * @param array<mixed> $array
223
     * @param string|int $key
224
     * @param mixed $default
225
     *
226
     * @return mixed|null
227
     */
228
    public static function remove(array &$array, string|int $key, mixed $default = null): mixed
229
    {
230
        if (isset($array[$key]) || array_key_exists($key, $array)) {
231
            $value = $array[$key];
232
233
            unset($array[$key]);
234
235
            return $value;
236
        }
237
238
        return $default;
239
    }
240
241
    /**
242
     * Return all of the given array except for a specified keys.
243
     * @param array<mixed> $array
244
     * @param array<int, int|string>|string|int $keys
245
     * @return array<mixed>
246
     */
247
    public static function except(array $array, array|string|int $keys): array
248
    {
249
        static::forget($array, $keys);
250
251
        return $array;
252
    }
253
254
    /**
255
     * Remove one or many array items from a given array using "dot" notation.
256
     * @param array<mixed> $array
257
     * @param array<int, int|string>|string|int $keys
258
     * @return void
259
     */
260
    public static function forget(array &$array, array|string|int $keys): void
261
    {
262
        $original = &$array;
263
264
        if (!is_array($keys)) {
0 ignored issues
show
introduced by
The condition is_array($keys) is always true.
Loading history...
265
            $keys = [$keys];
266
        }
267
268
        if (count($keys) === 0) {
269
            return;
270
        }
271
272
        foreach ($keys as $key) {
273
            if (static::exists($array, $key)) {
274
                unset($array[$key]);
275
276
                continue;
277
            }
278
279
            if (is_string($key)) {
280
                $parts = explode('.', $key);
281
282
                $array = &$original;
283
                while (count($parts) > 1) {
284
                    $part = array_shift($parts);
285
                    if (isset($array[$part]) && is_array($array[$part])) {
286
                        $array = &$array[$part];
287
                    } else {
288
                        continue 2;
289
                    }
290
                }
291
292
                unset($array[array_shift($parts)]);
293
            }
294
        }
295
    }
296
297
    /**
298
     * Get a value from the array, and remove it.
299
     * @param array<mixed> $array
300
     * @param string|int $key
301
     * @param mixed $default
302
     * @return mixed
303
     */
304
    public static function pull(array &$array, string|int $key, mixed $default = null): mixed
305
    {
306
        $value = static::get($array, $key, $default);
307
308
        static::forget($array, $key);
309
310
        return $value;
311
    }
312
313
    /**
314
     * Indexes and/or groups the array according to a specified key.
315
     * The input should be either multidimensional array or an array of objects.
316
     *
317
     * The $key can be either a key name of the sub-array, a property
318
     * name of object, or an anonymous function that must return the value
319
     * that will be used as a key.
320
     *
321
     * $groups is an array of keys, that will be used to group the input
322
     * array into one or more sub-arrays based on keys specified.
323
     *
324
     * If the `$key` is specified as `null` or a value of an element
325
     * corresponding to the key is `null` in addition to `$groups` not
326
     * specified then the element is discarded.
327
     *
328
     * @param array<mixed> $array
329
     * @param string|int|Closure|array<mixed>|null $key
330
     * @param string|int|string[]|int[]|Closure[]|null $groups
331
     * @return array<mixed> the indexed and/or grouped array
332
     */
333
    public static function index(
334
        array $array,
335
        string|int|Closure|array|null $key = null,
336
        string|int|array|null $groups = []
337
    ): array {
338
        $result = [];
339
        if (!is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
340
            $groups = (array) $groups;
341
        }
342
343
        foreach ($array as $element) {
344
            $lastArray = &$result;
345
            foreach ($groups as $group) {
346
                /** @var int|string $value */
347
                $value = static::getValue($element, $group);
348
                if (count($lastArray) > 0 && !array_key_exists($value, $lastArray)) {
349
                    $lastArray[$value] = [];
350
                }
351
                $lastArray = &$lastArray[$value];
352
            }
353
354
            if ($key === null) {
355
                if (count($groups) > 0) {
356
                    $lastArray[] = $element;
357
                }
358
            } else {
359
                $value = static::getValue($element, $key);
360
                if ($value !== null) {
361
                    if (is_float($value)) {
362
                        $value = (string) $value;
363
                    }
364
                    $lastArray[$value] = $element;
365
                }
366
            }
367
            unset($lastArray);
368
        }
369
370
        return $result;
371
    }
372
373
    /**
374
     * Returns the values of a specified column in an array.
375
     * The input array should be multidimensional or an array of objects.
376
     * @param array<mixed> $array
377
     * @param int|string|Closure $name
378
     * @param bool $keepKeys
379
     * @return array<mixed>
380
     */
381
    public static function getColumn(
382
        array $array,
383
        int|string|Closure $name,
384
        bool $keepKeys = true
385
    ): array {
386
        $result = [];
387
        if ($keepKeys) {
388
            foreach ($array as $key => $element) {
389
                $result[$key] = static::getValue($element, $name);
390
            }
391
        } else {
392
            foreach ($array as $element) {
393
                $result[] = static::getValue($element, $name);
394
            }
395
        }
396
397
        return $result;
398
    }
399
400
    /**
401
     * Builds a map (key-value pairs) from a multidimensional array or
402
     * an array of objects.
403
     * The `$from` and `$to` parameters specify the key names or property
404
     * names to set up the map.
405
     * Optionally, one can further group the map according to a
406
     * grouping field `$group`.
407
     *
408
     * @param array<mixed> $array
409
     * @param string|Closure $from
410
     * @param string|Closure $to
411
     * @param string|array<mixed>|Closure|null $group
412
     * @return array<mixed>
413
     */
414
    public static function map(
415
        array $array,
416
        string|Closure $from,
417
        string|Closure $to,
418
        string|array|Closure|null $group = null
419
    ): array {
420
        $result = [];
421
        foreach ($array as $element) {
422
            $key = static::getValue($element, $from);
423
            $value = static::getValue($element, $to);
424
            if ($group !== null) {
425
                $result[static::getValue($element, $group)][$key] = $value;
426
            } else {
427
                $result[$key] = $value;
428
            }
429
        }
430
431
        return $result;
432
    }
433
434
    /**
435
     * Checks if the given array contains the specified key.
436
     * This method enhances the `array_key_exists()` function by supporting
437
     * case-insensitive key comparison.
438
     *
439
     * @param string $key
440
     * @param array<mixed> $array
441
     * @param bool $caseSensative
442
     * @return bool
443
     */
444
    public static function keyExists(
445
        string $key,
446
        array $array,
447
        bool $caseSensative = true
448
    ): bool {
449
        if ($caseSensative) {
450
            return array_key_exists($key, $array);
451
        }
452
453
        foreach (array_keys($array) as $k) {
454
            if (strcasecmp($key, $k) === 0) {
455
                return true;
456
            }
457
        }
458
459
        return false;
460
    }
461
462
    /**
463
     * Sorts an array of objects or arrays (with the same structure) by one
464
     * or several keys.
465
     * @param array<mixed> $array
466
     * @param string|Closure|array<string> $key the key(s) to be sorted by.
467
     *     This refers to a key name of the sub-array elements, a property name
468
     *      of the objects, or an anonymous function returning the values
469
     *   for comparison purpose. The anonymous function signature
470
     * should be: `function($item)`.
471
     * @param int|array<int> $direction
472
     * @param int|array<int> $sortFlag
473
     * @return void
474
     */
475
    public static function multisort(
476
        array &$array,
477
        string|Closure|array $key,
478
        int|array $direction = SORT_ASC,
479
        int|array $sortFlag = SORT_REGULAR
480
    ): void {
481
        $keys = is_array($key) ? $key : [$key];
0 ignored issues
show
introduced by
The condition is_array($key) is always true.
Loading history...
482
483
        if (empty($keys) || empty($array)) {
484
            return;
485
        }
486
487
        $count  = count($keys);
488
        if (is_scalar($direction)) {
489
            $direction = array_fill(0, $count, $direction);
490
        } elseif (count($direction) !== $count) {
491
            throw new InvalidArgumentException(
492
                'The length of the sort direction must be the same '
493
                    . 'as that of sort keys.'
494
            );
495
        }
496
        if (is_scalar($sortFlag)) {
497
            $sortFlag = array_fill(0, $count, $sortFlag);
498
        } elseif (count($sortFlag) !== $count) {
499
            throw new InvalidArgumentException(
500
                'The length of the sort flag must be the same '
501
                    . 'as that of sort keys.'
502
            );
503
        }
504
505
        /** @var array<int, mixed> $args */
506
        $args = [];
507
        foreach ($keys as $i => $k) {
508
            $flag = $sortFlag[$i];
509
            $args[] = static::getColumn($array, $k);
510
            $args[] = $direction[$i];
511
            $args[] = $flag;
512
        }
513
514
        // This fix is used for cases when main sorting specified by
515
        // columns has equal values
516
        // Without it it will lead to Fatal Error: Nesting level
517
        // too deep - recursive dependency?
518
        $args[] = range(1, count($array));
519
        $args[] = SORT_ASC;
520
        $args[] = SORT_NUMERIC;
521
        $args[] = &$array;
522
523
        array_multisort(...$args);
524
    }
525
526
    /**
527
     * Check whether the given array is an associative array.
528
     *
529
     * An array is associative if all its keys are strings.
530
     * If `$allStrings` is false, then an array will be treated as associative
531
     * if at least one of its keys is a string.
532
     *
533
     * Note that an empty array will NOT be considered associative.
534
     *
535
     * @param array<mixed> $array
536
     * @param bool $allStrings
537
     * @return bool
538
     */
539
    public static function isAssoc(array $array, bool $allStrings = true): bool
540
    {
541
        if (empty($array)) {
542
            return false;
543
        }
544
545
        if ($allStrings) {
546
            foreach ($array as $key => $value) {
547
                if (!is_string($key)) {
548
                    return false;
549
                }
550
            }
551
552
            return true;
553
        } else {
554
            foreach ($array as $key => $value) {
555
                if (is_string($key)) {
556
                    return true;
557
                }
558
            }
559
560
            return false;
561
        }
562
    }
563
564
    /**
565
     * Whether the given array is an indexed array.
566
     *
567
     * An array is indexed if all its keys are integers.
568
     * If `$consecutive` is true, then the array keys must be a consecutive
569
     * sequence starting from 0.
570
     *
571
     * Note that an empty array will be considered indexed.
572
     *
573
     * @param array<mixed> $array
574
     * @param bool $consecutive
575
     * @return bool
576
     */
577
    public static function isIndexed(array $array, bool $consecutive = false): bool
578
    {
579
        if (empty($array)) {
580
            return true;
581
        }
582
583
        if ($consecutive) {
584
            return array_keys($array) === range(0, count($array) - 1);
585
        } else {
586
            foreach ($array as $key => $value) {
587
                if (!is_int($key)) {
588
                    return false;
589
                }
590
            }
591
592
            return true;
593
        }
594
    }
595
596
    /**
597
     * Check whether an array or Traversable contains an element.
598
     *
599
     * This method does the same as the PHP function in_array()
600
     * but additionally works for objects that implement the
601
     * Traversable interface.
602
     *
603
     * @param mixed $needle
604
     * @param array<mixed>|Traversable<int|string, mixed> $array
605
     * @param bool $strict
606
     * @return bool
607
     */
608
    public static function isIn(mixed $needle, array|Traversable $array, bool $strict = false): bool
609
    {
610
        if ($array instanceof Traversable) {
0 ignored issues
show
introduced by
$array is never a sub-type of Traversable.
Loading history...
611
            $array = iterator_to_array($array);
612
        }
613
614
        foreach ($array as $value) {
615
            if ($needle == $value && (!$strict || $needle === $value)) {
616
                return true;
617
            }
618
        }
619
620
621
        return in_array($needle, $array, $strict);
622
    }
623
624
    /**
625
     * Checks whether a variable is an array or Traversable.
626
     * @param mixed $var
627
     * @return bool
628
     */
629
    public static function isTraversable(mixed $var): bool
630
    {
631
        return is_array($var) || $var instanceof Traversable;
632
    }
633
634
    /**
635
     * Checks whether an array or Traversable is a subset of another array
636
     * or Traversable.
637
     *
638
     * This method will return `true`, if all elements of `$needles`
639
     * are contained in `$array`. If at least one element is missing,
640
     * `false` will be returned.
641
     *
642
     * @param array<mixed>|Traversable<int|string, mixed> $needles
643
     * @param array<mixed>|Traversable<int|string, mixed> $array
644
     * @param bool $strict
645
     * @return bool
646
     */
647
    public static function isSubset(
648
        array|Traversable $needles,
649
        array|Traversable $array,
650
        bool $strict = false
651
    ): bool {
652
        foreach ($needles as $needle) {
653
            if (!static::isIn($needle, $array, $strict)) {
654
                return false;
655
            }
656
        }
657
658
        return true;
659
    }
660
661
    /**
662
     * Filters array according to rules specified.
663
     * @param array<mixed> $array
664
     * @param array<string> $filters Rules that define array keys which should
665
     *  be left or removed from results.
666
     *         Each rule is:
667
     *            - `var` - `$array['var']` will be left in result.
668
     *            - `var.key` = only `$array['var']['key'] will be left in result.
669
     *            - `!var.key` = `$array['var']['key'] will be removed from result.
670
     * @return array<mixed>
671
     */
672
    public static function filter(array $array, array $filters): array
673
    {
674
        $result = [];
675
        $tobeRemoved = [];
676
677
        foreach ($filters as $filter) {
678
            $keys = explode('.', $filter);
679
            $globalKey = $keys[0];
680
            $localkey = $keys[1] ?? null;
681
            if ($globalKey[0] === '!') {
682
                $tobeRemoved[] = [
683
                    substr($globalKey, 1),
684
                    $localkey
685
                ];
686
687
                continue;
688
            }
689
690
            if (empty($array[$globalKey])) {
691
                continue;
692
            }
693
694
            if ($localkey === null) {
695
                $result[$globalKey] = $array[$globalKey];
696
                continue;
697
            }
698
699
            if (!isset($array[$globalKey][$localkey])) {
700
                continue;
701
            }
702
703
            if (array_key_exists($globalKey, $result)) {
704
                $result[$globalKey] = [];
705
            }
706
707
            $result[$globalKey][$localkey] = $array[$globalKey][$localkey];
708
        }
709
710
        foreach ($tobeRemoved as $value) {
711
            [$globalKey, $localkey] = $value;
712
            if (array_key_exists($globalKey, $result)) {
713
                unset($result[$globalKey][$localkey]);
714
            }
715
        }
716
717
        return $result;
718
    }
719
720
    /**
721
     * Checks whether a variable is an array accessible.
722
     * @param mixed $var
723
     * @return bool
724
     */
725
    public static function isAccessible(mixed $var): bool
726
    {
727
        return is_array($var) || $var instanceof ArrayAccess;
728
    }
729
730
    /**
731
     * Checks whether a variable is an array or instance of Arrayable.
732
     * @param mixed $var
733
     * @return bool
734
     */
735
    public static function isArrayable(mixed $var): bool
736
    {
737
        return is_array($var) || $var instanceof Arrayable;
738
    }
739
740
    /**
741
     * If the given value is not an array and not null, wrap it in one.
742
     * @param mixed $var
743
     * @return array<mixed>
744
     */
745
    public static function wrap(mixed $var): array
746
    {
747
        if ($var === null) {
748
            return [];
749
        }
750
        return is_array($var) ? $var : [$var];
751
    }
752
753
    /**
754
     * Check whether the given key exists in the provided array.
755
     *
756
     * @param array<mixed>|ArrayAccess<string|int, mixed> $array
757
     * @param string|int $key
758
     * @return bool
759
     */
760
    public static function exists(array|ArrayAccess $array, string|int $key): bool
761
    {
762
        if (is_array($array)) {
0 ignored issues
show
introduced by
The condition is_array($array) is always true.
Loading history...
763
            return array_key_exists($key, $array);
764
        }
765
766
        return $array->offsetExists($key);
767
    }
768
769
    /**
770
     * Get an item from an array using "dot" notation.
771
     *
772
     * @param array<mixed>|ArrayAccess<string|int, mixed> $array
773
     * @param string|int|null $key
774
     * @param mixed $default
775
     * @return mixed
776
     */
777
    public static function get(
778
        array|ArrayAccess $array,
779
        string|int|null $key = null,
780
        mixed $default = null
781
    ): mixed {
782
        if ($key === null) {
783
            return $array;
784
        }
785
786
        if (isset($array[$key])) {
787
            return $array[$key];
788
        }
789
790
        // Fix: If is int, stop continue find.
791
        if (!is_string($key)) {
792
            return $default;
793
        }
794
795
        foreach (explode('.', $key) as $segment) {
796
            if (static::isAccessible($array) && static::exists($array, $segment)) {
797
                $array = $array[$segment];
798
            } else {
799
                return $default;
800
            }
801
        }
802
803
        return $array;
804
    }
805
806
    /**
807
     * Check if an item exists in an array using "dot" notation.
808
     *
809
     * @param array<mixed>|ArrayAccess<string|int, mixed> $array
810
     * @param string|int $key
811
     * @return bool
812
     */
813
    public static function has(array|ArrayAccess $array, string|int $key): bool
814
    {
815
        if (empty($array)) {
816
            return false;
817
        }
818
819
        if (
820
            (is_array($array) && array_key_exists($key, $array))
821
            || ($array instanceof ArrayAccess && $array->offsetExists($key))
822
        ) {
823
            return true;
824
        }
825
826
        // Fix: If is int, stop continue find.
827
        if (!is_string($key)) {
828
            return false;
829
        }
830
831
        foreach (explode('.', $key) as $segment) {
832
            if (
833
                ((is_array($array) && array_key_exists($segment, $array))
834
                || ($array instanceof ArrayAccess
835
                        && $array->offsetExists($segment)))
836
            ) {
837
                $array = $array[$segment];
838
            } else {
839
                return false;
840
            }
841
        }
842
843
        return true;
844
    }
845
846
    /**
847
     *
848
     * @param array<mixed> $array
849
     * @param string|null $key
850
     * @param mixed $value
851
     * @return void
852
     */
853
    public static function set(array &$array, ?string $key, mixed $value): void
854
    {
855
        if ($key === null) {
856
            return;
857
        }
858
859
        $keys = explode('.', $key);
860
861
        while (count($keys) > 1) {
862
            $key = array_shift($keys);
863
864
            // If the key doesn't exist at this depth, we will just create
865
            // an empty array hold the next value, allowing us to create
866
            // the arrays to hold final values at the correct depth.
867
            // Then we'll keep digging into the array.
868
            if (!isset($array[$key]) || !is_array($array[$key])) {
869
                $array[$key] = [];
870
            }
871
872
            $array = &$array[$key];
873
        }
874
875
        $array[array_shift($keys)] = $value;
876
    }
877
878
    /**
879
     * Insert one array to another array
880
     *
881
     * @param array<mixed> $array
882
     * @param int $index
883
     * @param array<mixed> ...$inserts
884
     * @return void
885
     */
886
    public static function insert(
887
        array &$array,
888
        int $index,
889
        array ...$inserts
890
    ): void {
891
        $first = array_splice($array, 0, $index);
892
        $array = array_merge($first, $inserts, $array);
893
    }
894
895
    /**
896
     * Flatten a multi-dimensional array into a single level.
897
     * @param array<mixed> $array
898
     * @param int $depth
899
     * @return array<mixed>
900
     */
901
    public static function flatten(array $array, int $depth = PHP_INT_MAX): array
902
    {
903
        $result = [];
904
        foreach ($array as $item) {
905
            if (!is_array($item)) {
906
                $result[] = $item;
907
            } elseif ($depth === 1) {
908
                $result = array_merge($result, array_values($item));
909
            } else {
910
                $result = array_merge(
911
                    $result,
912
                    static::flatten($item, $depth - 1)
913
                );
914
            }
915
        }
916
917
        return $result;
918
    }
919
920
    /**
921
     * find similar text from an array
922
     *
923
     * @param string $need
924
     * @param array<int, string>|Traversable<int|string, mixed> $array
925
     * @param int $percentage
926
     * @return array<int, string>
927
     */
928
    public static function similar(
929
        string $need,
930
        array|Traversable $array,
931
        int $percentage = 45
932
    ): array {
933
        if (empty($need)) {
934
            return [];
935
        }
936
937
        $similar = [];
938
        $percent = 0;
939
        foreach ($array as $name) {
940
            similar_text($need, $name, $percent);
941
            if ($percentage <= (int) $percent) {
942
                $similar[] = $name;
943
            }
944
        }
945
946
        return $similar;
947
    }
948
949
    /**
950
     * Return the array key max width
951
     * @param array<int|string, mixed> $array
952
     * @param bool $expectInt
953
     * @return int
954
     */
955
    public static function getKeyMaxWidth(array $array, bool $expectInt = false): int
956
    {
957
        $max = 0;
958
        foreach ($array as $key => $value) {
959
            if (!$expectInt || !is_numeric($key)) {
960
                $width = mb_strlen((string)$key, 'UTF-8');
961
                if ($width > $max) {
962
                    $max = $width;
963
                }
964
            }
965
        }
966
967
        return $max;
968
    }
969
970
    /**
971
     * Return the first element in an array passing a given truth test.
972
     * @param array<mixed> $array
973
     * @param callable $callable
974
     * @param mixed $default
975
     *
976
     * @return mixed
977
     */
978
    public static function first(
979
        array $array,
980
        callable $callable = null,
981
        mixed $default = null
982
    ): mixed {
983
        if ($callable === null) {
984
            if (count($array) === 0) {
985
                return $default;
986
            }
987
988
            foreach ($array as $value) {
989
                return $value;
990
            }
991
        }
992
993
        foreach ($array as $key => $value) {
994
            if ($callable($value, $key)) {
995
                return $value;
996
            }
997
        }
998
999
        return $default;
1000
    }
1001
1002
    /**
1003
     * Return the last element in an array passing a given truth test.
1004
     * @param array<mixed> $array
1005
     * @param callable $callable
1006
     * @param mixed $default
1007
     *
1008
     * @return mixed
1009
     */
1010
    public static function last(
1011
        array $array,
1012
        callable $callable = null,
1013
        mixed $default = null
1014
    ): mixed {
1015
        if ($callable === null) {
1016
            if (count($array) === 0) {
1017
                return $default;
1018
            }
1019
1020
            return end($array);
1021
        }
1022
1023
        return static::first(array_reverse($array, true), $callable, $default);
1024
    }
1025
1026
    /**
1027
     * Filter the array using the given callback.
1028
     * @param array<mixed> $array
1029
     * @param callable $callable
1030
     * @return array<mixed>
1031
     */
1032
    public static function where(array $array, callable $callable): array
1033
    {
1034
        return array_filter($array, $callable, ARRAY_FILTER_USE_BOTH);
1035
    }
1036
1037
    /**
1038
     * Convert the array into a query string.
1039
     * @param array<mixed> $array
1040
     * @return string
1041
     */
1042
    public static function query(array $array): string
1043
    {
1044
        return http_build_query($array, '', '&', PHP_QUERY_RFC3986);
1045
    }
1046
1047
    /**
1048
     * Get a subset of the items from the given array.
1049
     * @param array<mixed> $array
1050
     * @param array<int, int|string> $keys
1051
     * @return array<mixed>
1052
     */
1053
    public static function only(array $array, array $keys): array
1054
    {
1055
        return array_intersect_key($array, array_flip($keys));
1056
    }
1057
1058
    /**
1059
     * Pluck an array of values from an arrays.
1060
     * @param array<int, array<mixed>> $array
1061
     * @param string|int $value
1062
     * @param string|int|null $key
1063
     * @return array<mixed>
1064
     */
1065
    public static function pluck(
1066
        array $array,
1067
        string|int $value,
1068
        string|int|null $key = null
1069
    ): array {
1070
        $results = [];
1071
        foreach ($array as $item) {
1072
            if (is_array($item)) {
1073
                $itemValue = static::get($item, $value);
1074
1075
                // If the key is "null", we will just append the value to the array
1076
                // and keep looping. Otherwise we will key the array using
1077
                // the value of the key we received from the developer.
1078
                // Then we'll return the final array form.
1079
                if ($key === null) {
1080
                    $results[] = $itemValue;
1081
                } else {
1082
                    $itemKey = static::get($item, $key);
1083
                    if (is_object($itemKey) && $itemKey instanceof Stringable) {
1084
                        $itemKey = (string)$itemKey;
1085
                    }
1086
1087
                    $results[$itemKey] = $itemValue;
1088
                }
1089
            }
1090
        }
1091
1092
        return $results;
1093
    }
1094
1095
    /**
1096
     * Collapse an array of arrays into a single array.
1097
     * @param array<mixed> $array
1098
     * @return array<mixed>
1099
     */
1100
    public static function collapse(array $array): array
1101
    {
1102
        $results = [];
1103
        foreach ($array as $values) {
1104
            if (!is_array($values)) {
1105
                continue;
1106
            }
1107
1108
            $results = array_merge($results, $values);
1109
        }
1110
1111
        return $results;
1112
    }
1113
1114
    /**
1115
     * Cross join the given arrays, returning all possible permutations.
1116
     * @param array<mixed> ...$arrays
1117
     * @return array<mixed>
1118
     */
1119
    public static function crossJoin(array ...$arrays): array
1120
    {
1121
        $results = [[]];
1122
        foreach ($arrays as $index => $array) {
1123
            $append = [];
1124
1125
            foreach ($results as $product) {
1126
                foreach ($array as $item) {
1127
                    $product[$index] = $item;
1128
1129
                    $append[] = $product;
1130
                }
1131
            }
1132
1133
            $results = $append;
1134
        }
1135
1136
        return $results;
1137
    }
1138
1139
    /**
1140
     *
1141
     * @param array<int|string, mixed> $array
1142
     * @param mixed $value
1143
     * @param mixed $key
1144
     * @return array<mixed>
1145
     */
1146
    public static function prepend(array $array, mixed $value, mixed $key = null): array
1147
    {
1148
        if ($key === null) {
1149
            array_unshift($array, $value);
1150
        } else {
1151
            $array = [$key => $value] + $array;
1152
        }
1153
1154
        return $array;
1155
    }
1156
1157
    /**
1158
     * Get one or a specified number of random values from an array.
1159
     * @param array<mixed> $array
1160
     * @param int|null $number
1161
     *
1162
     * @return mixed
1163
     */
1164
    public static function random(array $array, ?int $number = null): mixed
1165
    {
1166
        $requested = $number === null ? 1 : $number;
1167
        $count = count($array);
1168
1169
        if ($requested > $count) {
1170
            throw new InvalidArgumentException(sprintf(
1171
                'You requested %d items, but there are only %d items available',
1172
                $requested,
1173
                $count
1174
            ));
1175
        }
1176
1177
        if ($number === null) {
1178
            return $array[array_rand($array)];
1179
        }
1180
1181
        if ($number === 0) {
1182
            return [];
1183
        }
1184
1185
        $keys = array_rand($array, $number);
1186
1187
        $results = [];
1188
        foreach ((array)$keys as $key) {
1189
            $results[] = $array[$key];
1190
        }
1191
1192
        return $results;
1193
    }
1194
1195
    /**
1196
     * Convert an array to string
1197
     * @param array<mixed> $array
1198
     * @param string $glue
1199
     * @return string
1200
     */
1201
    public static function toString(array $array, string $glue = '_'): string
1202
    {
1203
        return implode($glue, $array);
1204
    }
1205
1206
    /**
1207
     * Shuffle the given array and return the result.
1208
     * @param array<mixed> $array
1209
     * @param int|null $seed
1210
     * @return array<mixed>
1211
     */
1212
    public static function shuffle(array $array, ?int $seed = null): array
1213
    {
1214
        if ($seed === null) {
1215
            shuffle($array);
1216
        } else {
1217
            mt_srand($seed);
1218
            shuffle($array);
1219
            mt_srand();
1220
        }
1221
1222
        return $array;
1223
    }
1224
1225
    /**
1226
     * Normalize command line arguments like splitting "-abc" and "--xyz=...".
1227
     * @param array<int, string> $args
1228
     * @return array<string>
1229
     */
1230
    public static function normalizeArguments(array $args): array
1231
    {
1232
        $normalized = [];
1233
        foreach ($args as $arg) {
1234
            if (preg_match('/^\-\w=/', $arg)) {
1235
                $normalized = array_merge(
1236
                    $normalized,
1237
                    (array)explode('=', $arg)
1238
                );
1239
            } elseif (preg_match('/^\-\w{2,}/', $arg)) {
1240
                $splitArgs = implode(' -', str_split(ltrim($arg, '-')));
0 ignored issues
show
Bug introduced by
It seems like str_split(ltrim($arg, '-')) can also be of type boolean; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1240
                $splitArgs = implode(' -', /** @scrutinizer ignore-type */ str_split(ltrim($arg, '-')));
Loading history...
1241
                $normalized = array_merge(
1242
                    $normalized,
1243
                    (array)explode(' ', '-' . $splitArgs)
1244
                );
1245
            } elseif (preg_match('/^\-\-([^\s\=]+)\=/', $arg)) {
1246
                $normalized = array_merge(
1247
                    $normalized,
1248
                    (array)explode('=', $arg)
1249
                );
1250
            } else {
1251
                $normalized[] = $arg;
1252
            }
1253
        }
1254
1255
        return $normalized;
1256
    }
1257
}
1258