Completed
Branch master (dadb56)
by Michał
02:12
created

Arr::pick()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 3
1
<?php namespace nyx\utils;
2
3
// External dependencies
4
use nyx\diagnostics;
5
6
/**
7
 * Arr
8
 *
9
 * The Arr class provides a few helper methods to make dealing with arrays easier.
10
 *
11
 * All methods which work with string delimited keys accept a string delimiter. If none is given (ie. null is passed),
12
 * the default delimiter (dot) set statically in this class will be used.
13
 *
14
 * Some code in this class can be simplified and some duplication could be avoided but it is laid out so that the
15
 * most common use cases are checked for first with performance being the priority.
16
 *
17
 * Some methods have aliases. To avoid the overhead please use the base methods, not the aliases. The methods which
18
 * have aliases are documented as such and each alias directly points to the base method.
19
 *
20
 * Note: This class is based on Laravel, FuelPHP, Lo-dash and a few others, but certain methods which appear in
21
 * those are not included since they would add overhead we consider 'not worth it' and don't want to encourage
22
 * the use thereof:
23
 *
24
 *   - Arr::each()                      -> use array_map() instead.
25
 *   - Arr::filter(), Arr::reject()     -> use array_filter() instead.
26
 *   - Arr::range()                     -> use range() instead.
27
 *   - Arr::repeat()                    -> use array_fill() instead.
28
 *   - Arr::search()                    -> use array_search() instead.
29
 *   - Arr::shuffle()                   -> use shuffle() instead.
30
 *   - Arr::size()                      -> use count() instead.
31
 *
32
 * @package     Nyx\Utils
33
 * @version     0.1.0
34
 * @author      Michal Chojnacki <[email protected]>
35
 * @copyright   2012-2016 Nyx Dev Team
36
 * @link        http://docs.muyo.io/nyx/utils/index.html
37
 * @todo        Arr::sort() and Arr:sortBy() (add sortBy() to core\traits\Collection as well).
38
 * @todo        Add ArrayObject support? How? Just strip the array type hints so as to not add overhead with checks?
39
 */
40
class Arr
41
{
42
    /**
43
     * The traits of the Arr class.
44
     */
45
    use traits\StaticallyExtendable;
46
47
    /**
48
     * @var string  The default delimiter to use to separate array dimensions.
49
     */
50
    public static $delimiter = '.';
51
52
    /**
53
     * Adds an element to the given array but only if it does not yet exist.
54
     *
55
     * Note: Null as value of an item is considered a non-existing item for the purposes
56
     *       of this method.
57
     *
58
     * @param   array   $array      The array to which the element should be added.
59
     * @param   string  $key        The key at which the value should be added.
60
     * @param   mixed   $value      The value of the element.
61
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
62
     */
63
    public static function add(array& $array, string $key, $value, string $delimiter = null)
64
    {
65
        if (null === static::get($array, $key, null, $delimiter)) {
66
            static::set($array, $key, $value, $delimiter);
67
        }
68
    }
69
70
    /**
71
     * Checks whether all elements in the given array pass the given truth test.
72
     *
73
     * Aliases:
74
     *  - @see Arr::every()
75
     *
76
     * @param   array       $array      The array to traverse.
77
     * @param   callable    $callback   The truth test the elements should pass.
78
     * @return  bool                    True when the elements passed the truth test, false otherwise.
79
     */
80
    public static function all(array $array, callable $callback) : bool
81
    {
82
        // Map the array and then search for a 'false' boolean. If none is found,
83
        // we assume all elements passed the test.
84
        return !in_array(false, array_map($callback, $array), true);
85
    }
86
87
    /**
88
     * Checks whether any of the elements in the given array passes the given truth test.
89
     *
90
     * Aliases:
91
     *  - @see Arr::some()
92
     *
93
     * @param   array       $array      The array to traverse.
94
     * @param   callable    $callback   The truth test the elements should pass.
95
     * @return  bool                    True when at least on the the elements passed the truth test, false
96
     *                                  otherwise.
97
     */
98
    public static function any(array $array, callable $callback) : bool
99
    {
100
        // Map the array and then search for a 'true' boolean. If at least one is found,
101
        // we assume at least one element passed the test.
102
        return in_array(true, array_map($callback, $array), true);
103
    }
104
105
    /**
106
     * Returns the average value of the given array.
107
     *
108
     * @param   array   $array      The array to traverse.
109
     * @param   int     $decimals   The number of decimals to return.
110
     * @return  float               The average value.
111
     */
112
    public static function average(array $array, int $decimals = 0) : float
113
    {
114
        return round((array_sum($array) / count($array)), $decimals);
115
    }
116
117
    /**
118
     * Removes all elements containing falsy values from the given array. The keys are preserved.
119
     *
120
     * See {@link http://php.net/manual/en/language.types.boolean.php} for information on which values evaluate
121
     * to false.
122
     *
123
     * @param   array   $array  The array to traverse.
124
     * @return  array           The resulting array.
125
     */
126
    public static function clean(array $array) : array
127
    {
128
        return array_filter($array, function($value) {
129
            return (bool) $value;
130
        });
131
    }
132
133
    /**
134
     * Collapses an array of arrays into a single array. Not recursive, ie. only the first dimension of arrays
135
     * will be collapsed down.
136
     *
137
     * Standard array_merge() rules apply - @link http://php.net/manual/en/function.array-merge.php -
138
     * non-array values with numeric keys will be appended to the resulting array in the order they are given, while
139
     * non-array values with non-numeric keys will have their keys preserved but the values may be overwritten by
140
     * the nested arrays being collapsed down if those contain values with the same non-numeric keys. Latter arrays
141
     * overwrite previous arrays' keys on collisions.
142
     *
143
     * @param   array   $array  The array to collapse.
144
     * @return  array           The resulting array.
145
     */
146
    public static function collapse(array $array) : array
147
    {
148
        $results = [];
149
150
        foreach ($array as $key => $item) {
151
152
            // Nested arrays will be merged in (non-recursively).
153
            if (is_array($item)) {
154
                $results = array_merge($results, $item); continue;
155
            }
156
157
            // Values with numeric keys will be appended in any case.
158
            if (is_int($key)) {
159
                $results[] = $item; continue;
160
            }
161
162
            // Non-numeric keys. If we've got the given key in $results already, it means it was merged
163
            // in from one of the nested arrays and those are meant to overwrite the initial values on collisions -
164
            // thus we're not going to do anything in that case.
165
            if (!array_key_exists($key, $results)) {
166
                $results[$key] = $item;
167
            }
168
        }
169
170
        return $results;
171
    }
172
173
    /**
174
     * Checks whether the given value is contained within the given array. Equivalent of a recursive in_array.
175
     * When you are sure you are dealing with a 1-dimensional array, use in_array instead to avoid the overhead.
176
     *
177
     * @param   array   $haystack   The array to search in.
178
     * @param   mixed   $needle     The value to search for.
179
     * @param   bool    $strict     Whether strict equality matches should be performed on the values.
180
     * @return  bool                True when the value was found in the array, false otherwise.
181
     */
182
    public static function contains(array $haystack, $needle, bool $strict = true)
183
    {
184
        foreach ($haystack as $value) {
185
            if ((!$strict && $needle == $value) || $needle === $value) {
186
                return true;
187
            }
188
189
            if (is_array($value) && static::contains($needle, $value, $strict)) {
190
                return true;
191
            }
192
        }
193
194
        return false;
195
    }
196
197
    /**
198
     * Flattens a multi-dimensional array using the given delimiter.
199
     *
200
     * @param   array   $array      The initial array.
201
     * @param   string  $prepend    A string that should be prepended to the keys.
202
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
203
     * @return  array               The resulting array.
204
     */
205
    public static function delimit(array $array, string $prepend = '', string $delimiter = null)
206
    {
207
        // Results holder.
208
        $results = [];
209
210
        // Which string delimiter should we use?
211
        if (null === $delimiter) {
212
            $delimiter = static::$delimiter;
213
        }
214
215
        foreach ($array as $key => $value) {
216
            if (is_array($value) && !empty($value)) {
217
                $results = array_merge($results, static::delimit($value, $prepend.$key.$delimiter));
218
            } else {
219
                $results[$prepend.$key] = $value;
220
            }
221
        }
222
223
        return $results;
224
    }
225
226
    /**
227
     * Alias for @see Arr::find()
228
     */
229
    public static function detect(array $array, callable $callback, $default = null)
230
    {
231
        return static::find($array, $callback, $default);
232
    }
233
234
    /**
235
     * Divides an array into two arrays - the first containing the keys, the second containing the values.
236
     *
237
     * @param   array   $array  The initial array.
238
     * @return  array           The resulting array.
239
     */
240
    public static function divide(array $array)
241
    {
242
        return [array_keys($array), array_values($array)];
243
    }
244
245
    /**
246
     * Alias for @see Arr::all()
247
     */
248
    public static function every(array $array, callable $callback)
249
    {
250
        return static::all($array, $callback);
251
    }
252
253
    /**
254
     * Returns a subset of the given array, containing all keys except for the ones specified.
255
     *
256
     * @param   array   $array  The initial array.
257
     * @param   array   $keys   An array of keys to exclude from the initial array.
258
     * @return  array
259
     */
260
    public static function except(array $array, array $keys)
261
    {
262
        return array_diff_key($array, array_flip($keys));
263
    }
264
265
    /**
266
     * Fetches a flattened array of an element nested in the initial array.
267
     *
268
     * @param   array   $array      The initial array.
269
     * @param   string  $key        The string delimited key.
270
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
271
     * @return  array               The resulting array.
272
     */
273
    public static function fetch(array $array, string $key, string $delimiter = null) : array
274
    {
275
        // Results holder.
276
        $results = [];
277
278
        // Which string delimiter should we use?
279
        if (null === $delimiter) {
280
            $delimiter = static::$delimiter;
281
        }
282
283
        foreach (explode($delimiter, $key) as $segment) {
284
            $results = [];
285
286
            foreach ($array as $value) {
287
                $value = (array) $value;
288
289
                $results[] = $value[$segment];
290
            }
291
292
            $array = array_values($results);
293
        }
294
295
        return array_values($results);
296
    }
297
298
    /**
299
     * Returns the first value which passes the given truth test.
300
     *
301
     * Aliases:
302
     *  - @see Arr::detect()
303
     *
304
     * @param   array       $array      The array to traverse.
305
     * @param   callable    $callback   The truth test the value should pass.
306
     * @param   mixed       $default    The default value to be returned if none of the elements passes the test.
307
     * @return  mixed
308
     */
309
    public static function find(array $array, callable $callback, $default = null)
310
    {
311
        foreach ($array as $key => $value) {
312
            if (call_user_func($callback, $key, $value)) {
313
                return $value;
314
            }
315
        }
316
317
        return $default;
318
    }
319
320
    /**
321
     * Returns the first element of the array, the first $elements of the array when $elements is a positive integer,
322
     * or the first element which passes the given truth test when the $elements is a callable.
323
     *
324
     * Aliases:  @see \nyx\utils\Arr::head(), \nyx\utils\Arr::take()
325
     * Opposite: @see \nyx\utils\Arr::last()
326
     *
327
     * @param   array           $array      The array to traverse.
328
     * @param   callable|int    $elements   The truth test the value should pass or an integer denoting how many
329
     *                                      of the initial elements of the array should be returned.
330
     *                                      When not given, the method will return the first element of the array.
331
     * @param   mixed           $default    The default value to be returned if none of the elements passes
332
     *                                      the test or the array is empty.
333
     * @throws  \UnderflowException         When more values are requested than there are items in the array.
334
     * @throws  \InvalidArgumentException   When $elements is neither a valid integer nor a callable.
335
     * @return  mixed
336
     */
337
    public static function first(array $array, $elements = null, $default = null)
338
    {
339
        if (empty($array)) {
340
            return $default;
341
        }
342
343
        // Most common use case - simply return the first value of the array.
344
        if (!isset($elements) || $elements === 1) {
345
            return reset($array);
346
        }
347
348
        // With a integer given, return a slice containing the first $callback elements.
349
        if (is_int($elements)) {
350
351
            if ($elements < 1) {
352
                throw new \InvalidArgumentException("At least 1 element must be requested, while [$elements] were requested.");
353
            }
354
355
            if ($elements > ($count = count($array))) {
356
                throw new \UnderflowException("Requested [$elements] items, but the structure only contains [$count] items.");
357
            }
358
359
            return array_slice($array, 0, $elements);
360
        }
361
362
        // With a callable given, return the first value which passes the given truth test.
363
        if (is_callable($elements)) {
364
            return static::find($array, $elements, $default);
365
        }
366
367
        throw new \InvalidArgumentException('Expected $callback to be a positive integer or a callable, got ['.diagnostics\Debug::getTypeName($elements).'] instead.');
368
    }
369
370
    /**
371
     * Flattens a multi-dimensional array.
372
     *
373
     * @param   array   $array  The initial array.
374
     * @return  array           The flattened array.
375
     */
376
    public static function flatten(array $array)
377
    {
378
        $results = [];
379
380
        array_walk_recursive($array, function($x) use (&$results) {
381
            $results[] = $x;
382
        });
383
384
        return $results;
385
    }
386
387
    /**
388
     * Returns a string delimited key from an array, with a default value if the given key does not exist. If null
389
     * is given instead of a key, the whole initial array will be returned.
390
     *
391
     * Note: Nested objects will be accessed as if they were arrays, eg. if "some.nested.object" is an object,
392
     *       then looking for "some.nested.object.property" will be handled just as a normal array would.
393
     *
394
     * @param   array           $array      The array to search in.
395
     * @param   string|array    $key        The string delimited key or a chain (array) of nested keys pointing
396
     *                                      to the desired key.
397
     * @param   mixed           $default    The default value.
398
     * @param   string          $delimiter  The delimiter to use when exploding the key into parts.
399
     * @return  mixed
400
     */
401
    public static function get(array $array, $key = null, $default = null, string $delimiter = null)
402
    {
403
        // Make loops easier for the end-user - return the initial array if the key is null instead of forcing
404
        // a valid value.
405
        if (!isset($key)) {
406
            return $array;
407
        }
408
409
        // Which string delimiter should we use?
410
        if (!isset($delimiter)) {
411
            $delimiter = static::$delimiter;
412
        }
413
414
        // If the key is string delimited, we need to explode it into an array of segments.
415
        $segments = is_array($key) ? $key : explode($delimiter, $key);
416
417
        // One dimension at a time.
418
        while ($segment = array_shift($segments)) {
419
420
            // If the current segment is a wildcard, make sure the it points to an array
421
            // and pluck the remaining segments from it.
422
            if ($segment === '*') {
423
                return is_array($array) ? static::pluck($array, $segments, $delimiter) : $default;
424
            }
425
426
            // Note: isset() is the cheapest condition to check for while being rather probable at the same time,
427
            // thus the seemingly unintuitive condition ordering.
428
            if (isset($array->{$segment})) {
429
                $array = $array->{$segment};
430
            } elseif (is_array($array) && array_key_exists($segment, $array)) {
431
                $array = $array[$segment];
432
            } else {
433
                return $default;
434
            }
435
        }
436
437
        return $array;
438
    }
439
440
    /**
441
     * Checks whether the given string delimited key exists in the array.
442
     *
443
     * Note: Nested objects will not be accessed as if they were arrays, ie. if "some.nested.object" is an object,
444
     *       not an array, then looking for "some.nested.object.key" will always return false.
445
     *
446
     * @param   array   $array      The array to search in.
447
     * @param   string  $key        The string delimited key.
448
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
449
     * @return  bool                True when the given key exists, false otherwise.
450
     */
451
    public static function has(array $array, string $key, string $delimiter = null)
452
    {
453
        // Which string delimiter should we use?
454
        if (null === $delimiter) {
455
            $delimiter = static::$delimiter;
456
        }
457
458
        foreach (explode($delimiter, $key) as $segment) {
459
            if (!is_array($array) || !array_key_exists($segment, $array)) {
460
                return false;
461
            }
462
463
            $array = $array[$segment];
464
        }
465
466
        return true;
467
    }
468
469
    /**
470
     * @see \nyx\utils\Arr::first()
471
     */
472
    public static function head(array $array, $callback = null, $default = null)
473
    {
474
        return static::first($array, $callback, $default);
475
    }
476
477
    /**
478
     * Returns all but the last value(s) of the given array.
479
     *
480
     * If a callable is passed, elements at the end of the array are excluded from the result as long as the
481
     * callback returns a truthy value. If a number is passed, the last n values are excluded from the result.
482
     *
483
     * @param   array           $array      The array to traverse.
484
     * @param   callable|int    $callback   The truth test the value should pass or an integer denoting how many
485
     *                                      of the final elements of the array should be excluded. The count is
486
     *                                      1-indexed, ie. if you want to exclude the last 2 elements, pass 2.
487
     * @param   mixed           $default    The default value to be returned if none of the elements passes the test.
488
     *                                      Only useful when $callback is a callable.
489
     * @return  mixed
490
     */
491
    public static function initial(array $array, $callback, $default = null)
492
    {
493
        // When given a callable, keep counting as long as the callable returns a truthy value.
494 View Code Duplication
        if (is_callable($callback)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
495
            $i = 0;
496
497
            foreach (array_reverse($array) as $key => $value) {
498
                if (!call_user_func($callback, $key, $value)) {
499
                    break;
500
                }
501
502
                $i++;
503
            }
504
505
            // If we didn't get at least a single truthy value, return the default.
506
            if ($i === 0) {
507
                return $default;
508
            }
509
510
            // Otherwise we're just gonna overwrite the $callback and proceed as if it were an integer in the
511
            // first place.
512
            $callback = $i;
513
        }
514
515
        // At this point we need a positive integer, 1 at minimum.
516
        return array_slice($array, 0, -(int) (!$callback ? 1 : abs($callback)));
517
    }
518
519
    /**
520
     * Checks whether the given array is an associative array.
521
     *
522
     * @param   array   $array  The array to check.
523
     * @return  bool            True when the array is associative, false otherwise.
524
     */
525
    public static function isAssociative(array $array) : bool
526
    {
527
        return array_keys($array) !== range(0, count($array) - 1);
528
    }
529
530
    /**
531
     * Checks whether the given array is a multidimensional array.
532
     *
533
     * @param   array   $array  The array to check.
534
     * @return  bool            True when the array has multiple dimensions, false otherwise.
535
     */
536
    public static function isMultidimensional(array $array) : bool
537
    {
538
        return count($array) !== count($array, COUNT_RECURSIVE);
539
    }
540
541
    /**
542
     * Returns the last element of the array, the final $callback elements of the array when $callback is a number,
543
     * or the last element which passes the given truth test when the $callback is a callable.
544
     *
545
     * @param   array               $array      The array to traverse.
546
     * @param   callable|int|bool   $callback   The truth test the value should pass or an integer / numeric string
547
     *                                          denoting how many of the final elements of the array should be returned.
548
     *                                          When *any* other value is given, the method will return the last
549
     *                                          element of the array.
550
     * @param   mixed               $default    The default value to be returned if none of the elements passes
551
     *                                          the test or the array is empty.
552
     * @return  mixed
553
     */
554
    public static function last(array $array, $callback = false, $default = null)
555
    {
556
        // Avoid some overhead at this point already if possible.
557
        if (empty($array)) {
558
            return $default;
559
        }
560
561
        // Most common use case - simply return the last value of the array.
562
        if (!$callback) {
563
            return end($array);
564
        }
565
566
        // With a callable given, return the last value which passes the given truth test.
567
        if (is_callable($callback)) {
568
            foreach (array_reverse($array) as $key => $value) {
569
                if (call_user_func($callback, $key, $value)) {
570
                    return $value;
571
                }
572
            }
573
574
            return $default;
575
        }
576
577
        // Return only the last element when abs(callback) equals 1, otherwise return the final $callback elements.
578
        return (-1 === $callback = -1 * abs((int) $callback)) ? end($array) : array_slice($array, $callback);
579
    }
580
581
    /**
582
     * Returns the biggest value from the given array.
583
     *
584
     * @param   array   $array  The array to traverse.
585
     * @return  mixed           The resulting value.
586
     */
587
    public static function max(array $array)
588
    {
589
        // Avoid some overhead at this point already if possible.
590
        if (empty($array)) {
591
            return null;
592
        }
593
594
        // Sort in a descending order.
595
        arsort($array);
596
597
        // Return the first element of the sorted array.
598
        return reset($array);
599
    }
600
601
    /**
602
     * Merges 2 or more arrays recursively. Differs in two important aspects from array_merge_recursive(), to
603
     * more closely resemble the standard behaviour of the non-recursive array_merge():
604
     *   - In case of 2 different values, when they are not arrays, the latter one overwrites the earlier instead
605
     *     of merging them into an array;
606
     *   - Non-conflicting numeric keys are left unchanged. In case of a conflict, the new value will be appended
607
     *     to the resulting array (not preserving its key).
608
     *
609
     * @param   array   $array      The initial array.
610
     * @param   array   ...$with    One or more (ie. separate arguments) arrays to merge in.
611
     * @return  array               The resulting merged array.
612
     */
613
    public static function merge(array $array, array ...$with) : array
614
    {
615
        foreach ($with as $arr) {
616
            foreach ($arr as $key => $value) {
617
                // Append numeric keys.
618
                if (is_int($key)) {
619
                    array_key_exists($key, $array) ? $array[] = $value : $array[$key] = $value;
620
                }
621
                // Merge multi-dimensional arrays recursively.
622
                elseif (is_array($value) && array_key_exists($key, $array) && is_array($array[$key])) {
623
                    $array[$key] = static::merge($array[$key], $value);
624
                } else {
625
                    $array[$key] = $value;
626
                }
627
            }
628
        }
629
630
        return $array;
631
    }
632
633
    /**
634
     * Returns the smallest value from the given array.
635
     *
636
     * @param   array   $array  The array to traverse.
637
     * @return  mixed           The resulting value.
638
     */
639
    public static function min(array $array)
640
    {
641
        // Avoid some overhead at this point already if possible.
642
        if (empty($array)) {
643
            return null;
644
        }
645
646
        // Sort in an ascending order.
647
        asort($array);
648
649
        // Return the first element of the sorted array.
650
        return reset($array);
651
    }
652
653
    /**
654
     * Returns a subset of the given array, containing only the specified keys.
655
     *
656
     * @param   array   $array  The initial array.
657
     * @param   array   $keys   An array of keys (the keys are expected to be values of this array).
658
     * @return  array
659
     */
660
    public static function only(array $array, array $keys) : array
661
    {
662
        return array_intersect_key($array, array_values($keys));
663
    }
664
665
    /**
666
     * Returns a random value from the given array, or $elements random values when $elements is a positive integer, or
667
     * the first random element that passes the given truth test when $elements is a callable.
668
     *
669
     * @param   array           $array      The array to search in.
670
     * @param   callable|int    $elements   The number of random values to return or a callable to return the first
671
     *                                      randomly shuffled element that passes the given truth test.
672
     * @param   mixed           $default    The default value to be returned if none of the elements passes
673
     *                                      the test or the array is empty.
674
     * @return  mixed
675
     */
676
    public static function pick(array $array, $elements = null, $default = null)
677
    {
678
        // There are *slightly* better performing alternatives, but simply shuffling and delegating
679
        // the actual picking to static::first() allows us to avoid a fair amount of duplicated code.
680
        shuffle($array);
681
682
        return static::first($array, $elements, $default);
683
    }
684
685
    /**
686
     * Given an array containing other arrays or objects, this method will look for the value with the given
687
     * key/property of $value within them and return a new array containing all values of said key from the
688
     * initial array. Essentially like fetching a column from a classic database table.
689
     *
690
     * When the optional $key parameter is given, the resulting array will be indexed by the values corresponding
691
     * to the given $key.
692
     *
693
     * @see     array_column()  A faster and simpler alternative, if you do not need to pluck data with support
694
     *                          for delimited keys or wildcards.
695
     *
696
     * @param   array           $array      The array to search in.
697
     * @param   string|array    $value      The key of the value to look for.
698
     * @param   string|array    $key        The key of the value to index the resulting array by.
699
     * @param   string          $delimiter  The delimiter to use when exploding the key into parts.
700
     * @return  array
701
     */
702
    public static function pluck(array $array, $value, $key = null, string $delimiter = null) : array
703
    {
704
        $results = [];
705
706
        // Which string delimiter should we use?
707
        if (!isset($delimiter)) {
708
            $delimiter = static::$delimiter;
709
        }
710
711
        foreach ($array as $item) {
712
713
            $itemValue = static::get($item, $value, $delimiter);
714
715
            // If the key given is null, the resulting array will contain numerically indexed keys.
716
            if (!isset($key)) {
717
                $results[] = $itemValue;
718
            }
719
            // Otherwise we are going use the value of the given key and use it in the resulting array as key
720
            // for the value determined earlier.
721
            else {
722
                $results[static::get($item, $key, $delimiter)] = $itemValue;
723
            }
724
        }
725
726
        return $results;
727
    }
728
729
    /**
730
     * Returns the value for a string delimited key from an array and then removes it.
731
     *
732
     * @param   array   $array      The array to search in.
733
     * @param   string  $key        The string delimited key.
734
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
735
     * @return  mixed
736
     */
737
    public static function pull(&$array, string $key, string $delimiter = null)
738
    {
739
        $value = static::get($array, $key, null, $delimiter);
740
741
        static::remove($array, $key, $delimiter);
742
743
        return $value;
744
    }
745
746
    /**
747
     * Removes a string delimited key from the given array.
748
     *
749
     * @param   array&  $array      The array to search in.
0 ignored issues
show
Documentation introduced by
The doc-type array& could not be parsed: Unknown type name "array&" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
750
     * @param   string  $key        The string delimited key.
751
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
752
     */
753
    public static function remove(array& $array, string $key, string $delimiter = null)
754
    {
755
        // Which string delimiter should we use?
756
        if (!isset($delimiter)) {
757
            $delimiter = static::$delimiter;
758
        }
759
760
        // Explode the key according to that delimiter.
761
        $keys = explode($delimiter, $key);
762
763
        while ($key = array_shift($keys)) {
764
            if (!isset($array[$key]) || !is_array($array[$key])) {
765
                return;
766
            }
767
768
            $array =& $array[$key];
769
        }
770
771
        unset($array[array_shift($keys)]);
772
    }
773
774
    /**
775
     * Returns all but the first value of the given array, all but the first elements for which the $callback
776
     * returns true if $callback is a callable, or all but the first $callback elements if $callback is a number.
777
     *
778
     * Aliases:
779
     *  - @see Arr::tail()
780
     *
781
     * @param   array               $array      The array to traverse.
782
     * @param   callable|int|bool   $callback   The truth test the value should pass or an integer denoting how many
783
     *                                          of the initial elements of the array should be excluded. The count
784
     *                                          is 1-indexed, ie. if you want to exclude the first 2 elements, pass 2.
785
     *                                          When a falsy value is given, the method will return all but the first
786
     *                                          element of the array.
787
     * @param   mixed               $default    The default value to be returned if none of the elements passes the
788
     *                                          test or the array contains no more than one item.
789
     * @return  mixed
790
     */
791
    public static function rest(array $array, $callback = false, $default = null)
792
    {
793
        // Avoid some overhead at this point already if possible. We need at least 2 elements in the array for
794
        // this method to make any usage sense.
795
        if (2 > count($array)) {
796
            return $default;
797
        }
798
799
        // For a falsy callback, return all but the first element of the array.
800
        if (!$callback) {
801
            return array_slice($array, 1);
802
        }
803
804
        // With a callable given, keep counting as long as the callable returns a truthy value.
805 View Code Duplication
        if (is_callable($callback)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
806
            $i = 0;
807
808
            foreach ($array as $key => $value) {
809
                if (!call_user_func($callback, $key, $value)) {
810
                    break;
811
                }
812
813
                $i++;
814
            }
815
816
            // If we didn't get at least a single truthy value, return the default.
817
            if ($i === 0) {
818
                return $default;
819
            }
820
821
            // Otherwise we're just gonna overwrite the $callback and proceed as if it were an integer in the
822
            // first place.
823
            $callback = $i;
824
        }
825
826
        // Return the final $callback elements.
827
        return array_slice($array, abs((int) $callback));
828
    }
829
830
    /**
831
     * Sets the given value for a string delimited key within the given array. If null is given instead of a key,
832
     * the whole initial array will be overwritten with the given value.
833
     *
834
     * @param   array   $array      The array to set the value in.
835
     * @param   string  $key        The string delimited key.
836
     * @param   mixed   $value      The value to set.
837
     * @param   string  $delimiter  The delimiter to use when exploding the key into parts.
838
     * @return  mixed
839
     */
840
    public static function set(array& $array, $key, $value, string $delimiter = null)
841
    {
842
        // Make loops easier for the end-user - overwrite the whole array if the key is null.
843
        if (null === $key) {
844
            return $array = $value;
845
        }
846
847
        // Which string delimiter should we use?
848
        if (null === $delimiter) {
849
            $delimiter = static::$delimiter;
850
        }
851
852
        // Explode the key according to that delimiter.
853
        $keys = explode($delimiter, $key);
854
855
        while (count($keys) > 1) {
856
            $key = array_shift($keys);
857
858
            if (!isset($array[$key]) || !is_array($array[$key])) {
859
                $array[$key] = [];
860
            }
861
862
            $array =& $array[$key];
863
        }
864
865
        return $array[array_shift($keys)] = $value;
866
    }
867
868
    /**
869
     * Alias for {@see static::any()}
870
     */
871
    public static function some(array $array, callable $callback) : bool
872
    {
873
        return static::any($array, $callback);
874
    }
875
876
    /**
877
     * Alias for {@see static::rest()}
878
     */
879
    public static function tail(array $array, $callback = false, $default = null)
880
    {
881
        return static::rest($array, $callback, $default);
882
    }
883
884
    /**
885
     * @see \nyx\utils\Arr::first()
886
     */
887
    public static function take(array $array, $callback = null, $default = null)
888
    {
889
        return static::first($array, $callback, $default);
890
    }
891
892
    /**
893
     * Returns an array based on the initial array with all occurrences of the passed values removed. Uses strict
894
     * equality comparisons.
895
     *
896
     * @param   array   $array      The array to traverse.
897
     * @param   mixed   ...$values  The values which should get removed.
898
     * @return  array
899
     */
900
    public static function without(array $array, ...$values) : array
901
    {
902
        return array_filter($array, function($value) use ($values) {
903
            return !in_array($value, $values, true);
904
        });
905
    }
906
}
907