Completed
Push — master ( ca0579...2ba251 )
by Michael
02:05
created

Arrgh::breakChain()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Arrgh;
4
5
use \Closure;
6
use \InvalidArgumentException;
7
8
/**
9
 * A chainable array API or a set of static functions, or both.
10
 *
11
 * Note: arr_* global functions are defined at the end of the file
12
 *
13
 * @method  collapse(array $input)
14
 * @method  contains(array $haystack, string $needle, string $key)
15
 * @method  except(array $input, array|string $keys)
16
 * @method  only(array $input, array|string $keys)
17
 * @method  map_assoc(array $input, \Closure $callable)
18
 * @method  sort_by(array $input, string $key)
19
 * @method  depth(array $input)
20
 * @method  even(array $input)
21
 * @method  first(array $input)
22
 * @method  get(array, $input, array|string, $path, bool $collapse)
23
 * @method  head(array $input)
24
 * @method  is_collection(array $input)
25
 * @method  last(array $input)
26
 * @method  odd(array $input)
27
 * @method  partition(array $input, \Closure $callable)
28
 * @method  tail(array $input)
29
 */
30
class Arrgh implements \ArrayAccess, \Iterator
31
{
32
    const PHP_SORT_DIRECTION_56 = 1;
33
    const PHP_SORT_DIRECTION_7 = -1;
34
35
    private $array;
36
    private $array_position;
37
    private $original_array;
38
    private $terminate;
39
    private $keep_once;
40
    private $last_value;
41
42
    private static $php_version;
43
    private static $php_sort_direction;
44
45
    /**
46
     * Creates a new Arrgh object
47
     *
48
     * @method __construct
49
     * @param  mixed      $array Optional parameter that can be either an array or another Arrgh object.
50
     */
51
    public function __construct($array = [])
52
    {
53
        $this->array = $array;
54
        $this->array_position = 0;
55
        if ($array instanceof Arrgh) {
56
            $this->array = $array->toArray();
57
        }
58
        $this->original_array = $this->array;
59
        $this->terminate = true;
60
    }
61
62
    /**
63
     * Invoke calls for Arrgh instances.
64
     * @internal
65
     */
66
    public function __call($method, $args)
67
    {
68
        return self::invoke($method, $args, $this);
69
    }
70
71
    /**
72
     * Returns a native array.
73
     *
74
     * @method toArray
75
     * @return array  Returns an array.
76
     */
77
    public function toArray()
78
    {
79
        $array = array_map(function($item) {
80
            if ($item instanceof Arrgh) {
81
                return $item->toArray();
82
            }
83
            return $item;
84
        }, $this->array);
85
        return $array;
86
    }
87
88
    /**
89
     * Tells Arrgh to either return iteself or a value where it would otherwise
90
     * had terminated the chain and return a value.
91
     *
92
     * The default behaviour is to break the chain on terminating methods like:
93
     *
94
     * - join
95
     * - pop
96
     *
97
     * @method keepChain
98
     * @param  bool      $value      Set true to keep and false to terminate.
99
     * @param  bool      $keep_once  Set true to automatically switch of again
100
     *                               after one call to a terminating method.
101
     * @see keep
102
     * @see keepOnce
103
     * @see breakChain
104
     * @return Arrgh\Arrgh self
105
     */
106
    public function keepChain($value = true, $keep_once = false)
107
    {
108
        $this->terminate = !$value;
109
        $this->keep_once = $keep_once;
110
        return $this;
111
    }
112
113
    /**
114
     * Tells Arrgh to return itself where it would otheriwse had terminated
115
     * the chain and returned a value.
116
     *
117
     * @method keep
118
     * @see keepChain
119
     * @return Arrgh\Arrgh self
120
     */
121
    public function keep()
122
    {
123
        return $this->keepChain(true);
124
    }
125
126
    /**
127
     * Tells Arrgh to return iteself where it would otherwise had terminated
128
     * the chain and return a value, but do it just once.
129
     *
130
     * @method keepOnce
131
     * @see keepChain
132
     * @return Arrgh\Arrgh self
133
     */
134
    public function keepOnce()
135
    {
136
        return $this->keepChain(true, true);
137
    }
138
139
    /**
140
     * Tells Arrgh to return to its normal behaviour and return values rather
141
     * than itself when terminating methods are called.
142
     *
143
     * @method breakChain
144
     * @see keepChain
145
     * @return Arrgh\Arrgh self
146
     */
147
    public function breakChain()
148
    {
149
        return $this->keepChain(false);
150
    }
151
152
    /**
153
     * ArrayAccess
154
     * @internal
155
     */
156
    public function offsetExists($offset)
157
    {
158
        return isset($this->array[$offset]);
159
    }
160
161
    /**
162
     * ArrayAccess
163
     * @internal
164
     */
165
    public function offsetGet($offset)
166
    {
167
        return isset($this->array[$offset]) ? $this->array[$offset] : null;
168
    }
169
170
    /**
171
     * ArrayAccess
172
     * @internal
173
     */
174
    public function offsetSet($offset, $value)
175
    {
176
        if (is_null($offset)) {
177
            $this->array[] = $value;
178
        } else {
179
            $this->array[$offset] = $value;
180
        }
181
    }
182
183
    /**
184
     * ArrayAccess
185
     * @internal
186
     */
187
    public function offsetUnset($offset)
188
    {
189
        unset($this->array[$offset]);
190
    }
191
192
    /**
193
     * Iterator
194
     * @internal
195
     */
196
    public function current()
197
    {
198
        $value = $this->array[$this->array_position];
199
        if (is_array($value)) {
200
            return new Arrgh($value);
201
        }
202
        return $value;
203
    }
204
205
    /**
206
     * Iterator
207
     * @internal
208
     */
209
    public function key()
210
    {
211
        return $this->array_position;
212
    }
213
214
    /**
215
     * Iterator
216
     * @internal
217
     */
218
    public function next()
219
    {
220
        ++$this->array_position;
221
    }
222
223
    /**
224
     * Iterator
225
     * @internal
226
     */
227
    public function rewind()
228
    {
229
        $this->array_position = 0;
230
    }
231
232
    /**
233
     * Iterator
234
     * @internal
235
     */
236
    public function valid()
237
    {
238
        return isset($this->array[$this->array_position]);
239
    }
240
241
    /**
242
     * Creates a new Arrgh object (chain)
243
     *
244
     * @param  mixed      $array Optional parameter that can be either an array or another Arrgh object.
245
     * @see chain
246
     * @return Arrgh\Arrgh       A new Arrgh object
247
     */
248
    public static function arr($array = [])
249
    {
250
        return new self($array);
251
    }
252
253
    /**
254
     * Creates a new Arrgh object (chain)
255
     *
256
     * @param  mixed      $array Optional parameter that can be either an array or another Arrgh object.
257
     * @see arr
258
     * @return Arrgh\Arrgh       A new Arrgh object
259
     */
260
    public static function chain($array = [])
261
    {
262
        return new self($array);
263
    }
264
265
    /**
266
     * Invoke calls for static Arrgh calls.
267
     * @internal
268
     */
269
    public static function __callStatic($method, $args)
270
    {
271
        if ($method[0] === "_") {
272
            $method = substr($method, 1);
273
            $_args = $args;
274
            $first_argument = array_shift($args);
275
            if (is_array($first_argument)) {
276
                return self::chain($first_argument)->$method(...$args);
277
            }
278
            return self::chain()->$method(...$_args);
279
        }
280
        return self::invoke($method, $args);
281
    }
282
283
    /**
284
     * Returns list of supported functions partitioned into types of functions.
285
     *
286
     * @method allFunctions
287
     * @return array       Associative array with function type as key point
288
     *                     to a list of functions of that type
289
     */
290
    public static function allFunctions()
291
    {
292
        return [
293
            "_arrgh"        => self::$arr_functions,
294
            "_call"         => self::$simple_functions,
295
            "_rotateRight"  => self::$reverse_functions,
296
            "_swapTwoFirst" => self::$swapped_functions,
297
            "_copy"         => self::$mutable_functions,
298
            "_copyMultiple" => self::$mutable_functions_multiple,
299
            "_copyValue"    => self::$mutable_value_functions,
300
        ];
301
    }
302
303
    /**
304
     * Given the PHP version this functions returns the sort integer to use
305
     * for equal values in functions like `usort`. Optionally you can pass in
306
     * the existing value like so:
307
     *
308
     * usort($input, function ($a, $b) {
309
     *     return Arrgh::getSortDirection($a - $b);
310
     * });
311
     *
312
     * This will ensure that the custom sort function will work in both PHP
313
     * version 5.6.x and 7.x
314
     *
315
     * @method getSortDirection
316
     * @param  integer           $direction An integer like value
317
     * @return integer                      Returns a sort integer for a sort or compare function
318
     */
319
    public static function getSortDirection($direction = null)
320
    {
321
        if (self::$php_version === null) {
322
            self::$php_version = explode(".", phpversion());
323
            self::$php_sort_direction = self::$php_version[0] >= 7 ? self::PHP_SORT_DIRECTION_7 : self::PHP_SORT_DIRECTION_56;
324
        }
325
        if ($direction === null || $direction === 0) {
326
            return self::$php_sort_direction;
327
        }
328
        return $direction;
329
    }
330
331
    /**
332
     * Wraps a callable with the purpose of fixing bad PHP sort implementations.
333
     *
334
     * @internal
335
     *
336
     * @method wrapCallable
337
     * @param  \Closure      $callable A sort function
338
     * @return \Closure                A new closeure
339
     */
340
    private static function wrapCallable(Closure $callable)
341
    {
342
        $direction = self::getSortDirection();
343
        return function($a, $b) use ($direction, $callable) {
344
            $result = $callable($a, $b);
345
            if ($result === 0) {
346
                return $direction;
347
            }
348
            return $result;
349
        };
350
    }
351
352
353
    /**
354
     * Transforms the incoming calls to native calls.
355
     *
356
     * @internal
357
     *
358
     * @method invoke
359
     * @param  string $method Name of method to invoke.
360
     * @param  array  $args   Arguments for method.
361
     * @param  Arrgh  $object Optionally invoke on $object.
362
     *
363
     * @return mixed          Can return anything.
364
     */
365
    private static function invoke($method, $args, Arrgh $object = null)
366
    {
367
        self::getSortDirection();
368
369
        list($matching_handler, $matching_function, $post_handler) = self::findFunction($method);
370
371
        switch ($matching_function) {
372
            case "asort":
373
                self::handleCaseAsort(
374
                    /* ref */ $matching_function,
375
                    /* ref */ $args
376
                );
377
                break;
378
            case "array_column":
379
                self::handleCaseArrayColumn(
380
                    /* ref */ $matching_handler,
381
                    /* ref */ $matching_function,
382
                    /* ref */ $post_handler,
383
                    /* ref */ $args
384
                );
385
                break;
386
            default:
387
                break;
388
        }
389
390
        // If chain unshift array onto argument stack
391
        if ($object && !in_array($matching_function, self::$starters)) {
392
            array_unshift($args, $object->array);
393
        }
394
395
        // If some arrays are Arrghs map to array or if callable, wrap it in
396
        // new callable with info about sort direction.
397
        $args = array_map(function($arg) use ($matching_function) {
398
            if ($arg instanceof Arrgh) {
399
                return $arg->array;
400
            } else if ($arg instanceof Closure) {
401
                if (in_array($matching_function, self::$reverse_result_functions) && self::$php_version[0] < 7) {
402
                    return self::wrapCallable($arg);
403
                }
404
            }
405
            return $arg;
406
        }, $args);
407
408
        // Invoke handler
409
        $matching_handler_derefed = $matching_handler; // bug in 7.0.3, internally the var is no longer a string but a pointer
410
        $result = self::$matching_handler_derefed($matching_function, $args, $object);
411
412
        // If a post handler is registered let it modify the result
413
        if ($post_handler) {
414
            $result = $post_handler($result);
415
        }
416
417
        if ($object) {
418
            if (in_array($matching_function, self::$terminators)) {
419
                if ($object->terminate) {
420
                    if (is_array($result)) {
421
                        return new Arrgh($result);
422
                    }
423
                    return $result;
424
                }
425
                if ($object->keep_once) {
426
                    $object->terminate = true;
427
                    $object->keep_once = false;
428
                }
429
                $object->last_value = $result;
430
                return $object;
431
            }
432
            $object->array = $result;
433
            return $object;
434
        }
435
        return $result;
436
    }
437
438
    /**
439
     * Based on input method finds handler, function and post handler.
440
     *
441
     * @internal
442
     *
443
     * @return array Returns a tuble of [handler, function, postHandler]
444
     */
445
    private static function findFunction($method)
446
    {
447
        $snake = strtolower(preg_replace('/\B([A-Z])/', '_\1', $method));
448
        $function_name = $snake;
449
        $function_name_prefixed = stripos($method, "array_") === 0 ? $snake : "array_" . $snake;
450
451
        $all_function_names = [$function_name, $function_name_prefixed];
452
        $all_functions      = self::allFunctions();
453
454
        $matching_handler  = null;
455
        $matching_function = null;
456
        $post_handler      = null;
457
        foreach ($all_functions as $handler => $functions) {
458
            foreach ($all_function_names as $function) {
459
                if (in_array($function, $functions)) {
460
                    $matching_handler  = $handler;
461
                    $matching_function = $function;
462
                    break 2;
463
                }
464
            }
465
        }
466
467
        if ($matching_function === null) {
468
            throw new InvalidArgumentException("Method {$method} doesn't exist");
469
        }
470
        return [$matching_handler, $matching_function, $post_handler];
471
    }
472
473
    /**
474
     * Handles special case: asort - In PHP5 reverses equals ("arsort" doen't mess up for some reason)
475
     *
476
     * @internal
477
     */
478
    private static function handleCaseAsort(&$matching_function, &$args)
479
    {
480
        $matching_function = "uasort";
481
        array_push($args, function($a, $b) { return strcasecmp($a, $b); });
482
    }
483
484
    /**
485
     * Handles special case: array_column - Native array_column filters away null values.
486
     *
487
     * That means you cannot use array_column for multisort since array size no longer matches.
488
     * This version of array_column returns null if the column is missing.
489
     *
490
     * @internal
491
     */
492
    private static function handleCaseArrayColumn(&$matching_handler, &$matching_function, &$post_handler, &$args)
493
    {
494
        $matching_handler  = "_rotateRight";
495
        $matching_function = "array_map";
496
        $column_array = $args[0];
497
        $column_key   = $args[1];
498
        if (count($args) === 3) {
499
            $column_id = $args[2];
500
            $column_ids_new = array_map(function($item) use ($column_id) {
501
                return isset($item[$column_id]) ? $item[$column_id] : null;
502
            }, $column_array);
503
            $post_handler = function($result) use ($column_ids_new) {
504
                return array_combine($column_ids_new, $result);
505
            };
506
        }
507
        $args = [$column_array];
508
        array_push($args, function($item) use ($column_key) {
509
            return isset($item[$column_key]) ? $item[$column_key] : null;
510
        });
511
    }
512
513
    /**
514
     * Handler: _call
515
     *
516
     * Calls the native function directly.
517
     *
518
     * @internal
519
     */
520
    private static function _call($function, $args)
521
    {
522
        return $function(...$args);
523
    }
524
525
    /**
526
     * Handler: _rotateRight
527
     *
528
     * Shifts of the first argument (callable) and pushes it to the end.
529
     *
530
     * @internal
531
     */
532
    private static function _rotateRight($function, $args)
533
    {
534
        $first_argument = array_pop($args);
535
        array_unshift($args, $first_argument);
536
        return $function(...$args);
537
    }
538
539
    /**
540
     * Handler: _swapTwoFirst
541
     *
542
     * Swaps the first two args.
543
     *
544
     * @internal
545
     */
546
    private static function _swapTwoFirst($function, $args)
547
    {
548
        $first_argument = array_shift($args);
549
        $second_argument = array_shift($args);
550
        array_unshift($args, $first_argument);
551
        array_unshift($args, $second_argument);
552
        return $function(...$args);
553
    }
554
555
    /**
556
     * Handler: _copy
557
     *
558
     * Makes a copy of the array and returns it after invoking function.
559
     *
560
     * @internal
561
     */
562
    private static function _copy($function, $args)
563
    {
564
        $array = array_shift($args);
565
        $function($array, ...$args);
566
        return $array;
567
    }
568
569
    /**
570
     * Handler: _copyMultiple
571
     *
572
     * If multiple arrays are passed as arguments mulitple will be returned.
573
     * Otherwise _copy is used.
574
     *
575
     * @internal
576
     */
577
    private static function _copyMultiple($function, $args)
578
    {
579
        $function(...$args);
580
        $arrays = [];
581
        foreach ($args as $arg) {
582
            if (is_array($arg)) {
583
                $arrays[] = $arg;
584
            }
585
        }
586
        if (count($arrays) === 1) {
587
            return $arrays[0];
588
        }
589
        return $arrays;
590
    }
591
592
    /**
593
     * Handler: _copyValue
594
     *
595
     * Makes a copy of the array and returns it after invoking function.
596
     *
597
     * @internal
598
     */
599
    private static function _copyValue($function, $args, $object = null)
600
    {
601
        $array = array_shift($args);
602
        $result = $function($array, ...$args);
603
        if ($object) {
604
            $object->array = $array;
605
        }
606
        return $result;
607
    }
608
609
    /**
610
     * Handler: _arrgh
611
     *
612
     * The handler for non-native functions
613
     *
614
     * @internal
615
     */
616
    private static function _arrgh($function, $args)
617
    {
618
        $function = "arr_" . $function;
619
        return self::$function(...$args);
620
    }
621
622
    /**
623
     * A mapping function for associative arrays (keeps keys).
624
     *
625
     * @method map_assoc
626
     *
627
     * @param  array         $array    Array or array-like value.
628
     * @param  Closure       $callable Mapping function
629
     *
630
     * @return array                   Returns mapped associative array.
631
     */
632
    private static function arr_map_assoc($array, Closure $callable)
633
    {
634
        $keys = array_keys($array);
635
        return array_combine($keys, array_map($callable, $keys, $array));
636
    }
637
638
    /**
639
     * Sort an array of associative arrays by key. It checks the first two values for type
640
     * either sorts by number or using strcmp. If a key is missing entries are moved to the top
641
     * (or bottom depending on $direction)
642
     */
643
    private static function arr_sort_by($array, $key, $direction = "ASC")
644
    {
645
        $direction_int = strtoupper($direction) === "ASC" ? 1 : -1;
646
647
        if ($key instanceof Closure) {
648
            usort($array, self::wrapCallable($key));
649
            if ($direction_int === -1) {
650
                return array_reverse($array);
651
            }
652
            return $array;
653
        }
654
655
        $column = array_map(function($item) use ($key) {
656
            return isset($item[$key]) ? $item[$key] : null;
657
        }, $array);
658
        array_multisort($column, ($direction_int === 1 ? SORT_ASC : SORT_DESC), $array);
659
        return $array;
660
    }
661
662
    private static function arr_collapse($array)
663
    {
664
        return array_reduce($array, function($merged, $item) {
665
            if (is_array($item)) {
666
                return array_merge($merged, $item);
667
            }
668
            $merged[] = $item;
669
            return $merged;
670
        }, []);
671
    }
672
673
    private static function arr_contains($array, $search, $key = null)
674
    {
675
        if ($key) {
676
            $haystack = array_column($array, $key);
677
        } else {
678
            $haystack = array_reduce($array, function($merged, $item) {
679
                return array_merge($merged, array_values($item));
680
            }, []);
681
        }
682
        return array_search($search, $haystack) !== false;
683
    }
684
685
    private static function arr_except($array, $except)
686
    {
687
        if (is_string($except)) {
688
            $except = [$except];
689
        }
690
691
        $is_collection = self::arr_is_collection($array);
692
        $array = $is_collection ? $array : [$array];
693
694
        $result = array_map(function($item) use ($except) {
695
            foreach ($except as $key) {
696
                unset($item[$key]);
697
            }
698
            return $item;
699
        }, $array);
700
701
        if ($is_collection) {
702
            return $result;
703
        }
704
        return $result[0];
705
    }
706
707
    private static function arr_only($array, $only)
708
    {
709
        if (is_string($only)) {
710
            $only = [$only];
711
        }
712
713
        $is_collection = self::arr_is_collection($array);
714
        $array = $is_collection ? $array : [$array];
715
716
        $result = array_map(function($item) use ($only) {
717
            foreach ($item as $key => $value) {
718
                if (!in_array($key, $only)) {
719
                    unset($item[$key]);
720
                }
721
            }
722
            return $item;
723
        }, $array);
724
725
        if ($is_collection) {
726
            return $result;
727
        }
728
        return $result[0];
729
    }
730
731
    /**
732
     *  Get for multi-dimensional arrays
733
     *
734
     *  @param array      An array to query on
735
     *  @param path|array A string representing the path to traverse.
736
     *                    Optionally pass as [ $path, ...$functions ] if `!$` is used
737
     *  @param bool       Collapse resulting data-set
738
     *  @throws InvalidArgumentException Thrown when a path cannot be reached in case $array does
739
     *                    not correspond to path type. E.g. collection expected
740
     *                    but a simple value was encountered.
741
     */
742
    private static function arr_get($array, $path, $collapse = false)
743
    {
744
        $path_string = $path;
745
        if (is_array($path)) {
746
            $path_string = array_shift($path);
747
        }
748
        $path_segments = explode(".", $path_string);
749
        return self::_arr_get_traverse($array, $path_segments, $collapse, /* functions */ $path);
750
    }
751
752
    /* arr_get: Traverses path to get value */
753
    private static function _arr_get_traverse($data, $path, $collapse = false, $functions = [])
754
    {
755
        $next_key      = array_shift($path);
756
        $plug_index    = is_numeric($next_key) ? (int) $next_key : null;
757
        $is_collection = self::isCollection($data);
758
759
        // Apply custom function
760
        if ($next_key === '!$') {
761
            if ($is_collection) {
762
                list($data, $path, $functions, $next_key) = self::_arr_get_traverse_apply_custom_function($data, $functions, $path);
763
            } else {
764
                throw new InvalidArgumentException("Invalid path trying to invoke function on non-collection");
765
            }
766
        }
767
768
        // Select data either by index or key
769
        if ($plug_index === null) {
770
            $next_node = self::_arr_get_traverse_next_node_key($data, $is_collection, $next_key);
771
        } else {
772
            if ($is_collection) {
773
                $next_node = self::_arr_get_traverse_next_node_index($data, $plug_index);
774
            } else {
775
                throw new InvalidArgumentException("Invalid path trying to plug item but data is not a collection");
776
            }
777
        }
778
779
        // If nothing matched break path and return
780
        if (empty($next_node)) {
781
            return null;
782
        }
783
784
        // If path is at the end return
785
        if (count($path) === 0) {
786
            if (is_array($next_node) && $collapse) {
787
                return array_filter($next_node);
788
            }
789
            return $next_node;
790
        }
791
792
        // If path is not completed
793
        if (is_array($next_node)) {
794
795
            // Recurse
796
            if (self::arr_is_collection($next_node)) {
797
                $result = self::_arr_get_traverse_collection($path, $next_node, $collapse, $functions);
798
            } else {
799
                $result = self::_arr_get_traverse($next_node, $path, $collapse, $functions);
800
            }
801
802
            // Collapse result if needed
803
            if (is_array($result)) {
804
                // Collapse collections greater than 1
805
                if (self::arr_depth($result) > 1) {
806
                    $result = self::arr_collapse($result);
807
                }
808
                return array_filter($result);
809
            }
810
            return $result;
811
        }
812
        return null;
813
    }
814
815
    private static function _arr_get_traverse_collection($path, $next_node, $collapse, $functions)
816
    {
817
        $node_depth = self::arr_depth($next_node);
818
819
        // Collapse collections
820
        $is_intermediary_collection = !is_numeric($path[0]) && $path[0] !== "!$" && $node_depth > 0;
821
        if ($collapse && $is_intermediary_collection) {
822
            $next_node = self::arr_collapse($next_node);
823
        }
824
825
        if (is_numeric($path[0]) && $node_depth < 1) {
826
            $result = self::_arr_get_traverse($next_node, $path, $collapse, $functions);
827
        } else {
828
            // Collect data from sub-tree
829
            $result = [];
830
            foreach ($next_node as $node) {
831
                if ($node === null) {
832
                    $result[] = null;
833
                } else {
834
                    $partial = self::_arr_get_traverse($node, $path, $collapse, $functions);
835
                    if ($collapse) {
836
                        $result[] = $partial;
837
                    } else {
838
                        $result[] = [$partial];
839
                    }
840
                }
841
            }
842
        }
843
844
        // Since collection functions inject an array segment we must collapse the result
845
        if ($path[0] === "!$") {
846
            $result = self::arr_collapse($result);
847
        }
848
        return $result;
849
    }
850
851
    /* arr_get: Find next node by index */
852
    private static function _arr_get_traverse_next_node_index($data, $plug_index)
853
    {
854
        // Adjust negative index
855
        if ($plug_index < 0) {
856
            $count = count($data);
857
            $plug_index = $count === 1 ? 0 : $count + ($plug_index % $count);
858
        }
859
860
        // Plug data
861
        if (isset($data[$plug_index])) {
862
            return $data[$plug_index];
863
        }
864
        return null;
865
    }
866
867
    /* arr_get: Find next node by key */
868
    private static function _arr_get_traverse_next_node_key($data, $is_collection, $next_key)
869
    {
870
        if ($next_key === null) {
871
            return $data;
872
        }
873
        if ($is_collection) {
874
            return array_map(function($item) use ($next_key) {
875
                if ($item !== null && array_key_exists($next_key, $item)) {
876
                    return $item[$next_key];
877
                }
878
                return null;
879
            }, $data);
880
        } else if (is_array($data)) {
881
            if (array_key_exists($next_key, $data)) {
882
                return $data[$next_key];
883
            }
884
            return null;
885
        }
886
        throw new InvalidArgumentException("Path ...$next_key does not exist");
887
    }
888
889
    /* arr_get: Invoke custom filter function on path */
890
    private static function _arr_get_traverse_apply_custom_function($data, $functions, $path)
891
    {
892
        $function  = array_shift($functions);
893
        $data      = array_values(array_filter($data, $function, ARRAY_FILTER_USE_BOTH));
894
        $next_key  = array_shift($path);
895
        return [$data, $path, $functions, $next_key];
896
    }
897
898
    private static function arr_is_collection($mixed)
899
    {
900
        return is_array($mixed) && array_values($mixed) === $mixed;
901
    }
902
903
    /**
904
     * Return the depth of a collection hiearchy. Zero based.
905
     *
906
     * @param array A collection
907
     * @return int `null` if $array is not a collection.
908
     */
909
    private static function arr_depth($array)
910
    {
911
        // Empty arrays are assumed to be collections, thus returning 0-depth
912
        if (empty($array) && is_array($array)) {
913
            return 0;
914
        }
915
916
        // Associative arrays are not collections, return null
917
        if (!self::arr_is_collection($array)) {
918
            return null;
919
        }
920
921
        $depth = 0;
922
        $child = array_shift($array);
923
        while (self::arr_is_collection($child)) {
924
            $depth += 1;
925
            $child = array_shift($child);
926
        }
927
        return $depth;
928
    }
929
930
    /**
931
     * Partion the input based on the result of the callback function.
932
     *
933
     * @param array     $array    A collection
934
     * @param \Closeure $callable A callable returning true or false depending on which way to partion the element—left or right.
935
     * @return array An array with two arrays—left and right: [left, right]
936
     */
937
    private static function arr_partition($array, Closure $callable)
938
    {
939
        $left = [];
940
        $right = [];
941
        array_walk($array, function($item, $key) use (&$left, &$right, $callable) {
942
            if ($callable($item, $key)) {
943
                $left[] = $item;
944
            } else {
945
                $right[] = $item;
946
            }
947
        });
948
        return [$left, $right];
949
    }
950
951
    private static function arr_even($array)
952
    {
953
        return self::arr_partition($array, function($item, $key) { return $key % 2 === 0; })[0];
954
    }
955
956
    private static function arr_odd($array)
957
    {
958
        return self::arr_partition($array, function($item, $key) { return $key % 2 === 1; })[0];
959
    }
960
961
    /* Synonym of shift */
962
    private static function arr_head($array)
963
    {
964
        return self::shift($array);
965
    }
966
967
    private static function arr_first($array)
968
    {
969
        if (count($array)) {
970
            return $array[0];
971
        }
972
        return null;
973
    }
974
975
    private static function arr_last($array)
976
    {
977
        if (count($array)) {
978
            return $array[count($array) - 1];
979
        }
980
        return null;
981
    }
982
983
    private static function arr_tail($array)
984
    {
985
        return self::chain($array)->keep()->shift()->toArray();
0 ignored issues
show
Documentation Bug introduced by
The method shift does not exist on object<Arrgh\Arrgh>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
986
    }
987
988
    // _arrgh
989
    static private $arr_functions = [
990
        "collapse",
991
        "contains",
992
        "except",
993
        "map_assoc",
994
        "only",
995
        "sort_by",
996
        'depth',
997
        'even',
998
        'first',
999
        'get',
1000
        'head',
1001
        'is_collection',
1002
        'last',
1003
        'odd',
1004
        'partition',
1005
        'tail',
1006
    ];
1007
1008
    // _call
1009
    static private $simple_functions = [
1010
        "array_change_key_case",
1011
        "array_chunk",
1012
        "array_column",
1013
        "array_combine",
1014
        "array_count_values",
1015
        "array_diff",
1016
        "array_diff_assoc",
1017
        "array_diff_key",
1018
        "array_diff_uassoc",
1019
        "array_diff_ukey",
1020
        "array_fill",
1021
        "array_fill_keys",
1022
        "array_filter",
1023
        "array_flip",
1024
        "array_intersect",
1025
        "array_intersect_assoc",
1026
        "array_intersect_key",
1027
        "array_intersect_uassoc",
1028
        "array_intersect_ukey",
1029
        "array_keys",
1030
        "array_merge",
1031
        "array_merge_recursive",
1032
        "array_pad",
1033
        "array_product",
1034
        "array_rand",
1035
        "array_reduce",
1036
        "array_replace",
1037
        "array_replace_recursive",
1038
        "array_reverse",
1039
        "array_slice",
1040
        "array_sum",
1041
        "array_udiff",
1042
        "array_udiff_assoc",
1043
        "array_udiff_uassoc",
1044
        "array_uintersect",
1045
        "array_uintersect_assoc",
1046
        "array_uintersect_uassoc",
1047
        "array_unique",
1048
        "array_values",
1049
        "count",
1050
        "max",
1051
        "min",
1052
        "range",
1053
        "sizeof",
1054
    ];
1055
1056
    // _copy
1057
    static private $mutable_functions = [
1058
        "array_push",
1059
        "array_splice",
1060
        "array_unshift",
1061
        "array_walk",
1062
        "array_walk_recursive",
1063
        "arsort",
1064
        "asort",
1065
        "krsort",
1066
        "ksort",
1067
        "natcasesort",
1068
        "natsort",
1069
        "rsort",
1070
        "shuffle",
1071
        "sort",
1072
        "uasort",
1073
        "uksort",
1074
        "usort",
1075
    ];
1076
1077
    // _copyMultiple
1078
    static private $mutable_functions_multiple = [
1079
        "array_multisort",
1080
    ];
1081
1082
    // _copyValue
1083
    static private $mutable_value_functions = [
1084
        "array_pop",
1085
        "array_shift",
1086
        "end",
1087
    ];
1088
1089
    // _rotateRight
1090
    static private $reverse_functions = [
1091
        "array_map",
1092
    ];
1093
1094
    // _swapTwoFirst
1095
    static private $swapped_functions = [
1096
        "array_key_exists",
1097
        "array_search",
1098
        "implode",
1099
        "in_array",
1100
        "join",
1101
    ];
1102
1103
    static private $starters = [
1104
        "array_fill",
1105
        "array_fill_keys",
1106
        "range",
1107
    ];
1108
1109
    static private $terminators = [
1110
        "array_pop",
1111
        "array_shift",
1112
        "array_sum",
1113
        "count",
1114
        "first",
1115
        "head",
1116
        "join",
1117
        "last",
1118
        "max",
1119
        "min",
1120
        "sizeof",
1121
    ];
1122
1123
    static private $reverse_result_functions = [
1124
        "uasort",
1125
        "uksort",
1126
        "usort",
1127
        "asort",
1128
    ];
1129
}
1130