Arrgh::keep()   A
last analyzed

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