Completed
Push — master ( e2a796...d348bd )
by Banciu N. Cristian Mihai
35:29
created

DotArray::where()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 4
nop 1
dl 0
loc 23
rs 9.5555
c 0
b 0
f 0
1
<?php
2
3
namespace BinaryCube\DotArray;
4
5
/**
6
 * DotArray
7
 *
8
 * @package BinaryCube\DotArray
9
 * @author  Banciu N. Cristian Mihai <[email protected]>
10
 * @license https://github.com/binary-cube/dot-array/blob/master/LICENSE <MIT License>
11
 * @link    https://github.com/binary-cube/dot-array
12
 */
13
class DotArray implements
14
    \ArrayAccess,
15
    \IteratorAggregate,
16
    \Serializable,
17
    \JsonSerializable,
18
    \Countable
19
{
20
21
    /**
22
     * Unique object identifier.
23
     *
24
     * @var string
25
     */
26
    protected $uniqueIdentifier;
27
28
    /**
29
     * Config.
30
     *
31
     * @var array
32
     */
33
    protected $config = [
34
        'path' => [
35
            'template'  => '#(?|(?|[<token-start>](.*?)[<token-end>])|(.*?))(?:$|\.+)#i',
36
            'wildcards' => [
37
                '<token-start>' => ['\'', '\"', '\[', '\(', '\{'],
38
                '<token-end>'   => ['\'', '\"', '\]', '\)', '\}'],
39
            ],
40
        ],
41
    ];
42
43
    /**
44
     * The pattern that allow to match the JSON paths that use the dot notation.
45
     *
46
     * Allowed tokens for more complex paths: '', "", [], (), {}
47
     * Examples:
48
     *
49
     * - foo.bar
50
     * - foo.'bar'
51
     * - foo."bar"
52
     * - foo.[bar]
53
     * - foo.(bar)
54
     * - foo.{bar}
55
     *
56
     * Or more complex:
57
     * - foo.{bar}.[component].{version.1.0}
58
     *
59
     * @var string
60
     */
61
    protected $nestedPathPattern;
62
63
    /**
64
     * Stores the original data.
65
     *
66
     * @var array
67
     */
68
    protected $items;
69
70
71
    /**
72
     * Creates an DotArray object.
73
     *
74
     * @param mixed $items
75
     *
76
     * @return static
77
     */
78
    public static function create($items)
79
    {
80
        return (new static($items));
81
    }
82
83
84
    /**
85
     * @param string $json
86
     *
87
     * @return static
88
     */
89
    public static function createFromJson($json)
90
    {
91
        return static::create(\json_decode($json, true));
92
    }
93
94
95
    /**
96
     * Return the given items as an array
97
     *
98
     * @param mixed $items
99
     *
100
     * @return array
101
     */
102
    protected static function normalize(&$items)
103
    {
104
        if (\is_array($items)) {
105
            return $items;
106
        } else if (empty($items)) {
107
            return [];
108
        } else if ($items instanceof self) {
109
            return $items->toArray();
110
        }
111
112
        return (array) $items;
113
    }
114
115
116
    /**
117
     * List with internal operators and the associated callbacks.
118
     *
119
     * @return array
120
     */
121
    protected static function operators()
122
    {
123
        return [
124
            [
125
                'tokens' => ['=', '==', 'eq'],
126
                'closure' => function ($item, $property, $value) {
127
                    return $item[$property] == $value[0];
128
                },
129
            ],
130
131
            [
132
                'tokens' => ['===', 'i'],
133
                'closure' => function ($item, $property, $value) {
134
                    return $item[$property] === $value[0];
135
                },
136
            ],
137
138
            [
139
                'tokens' => ['!=', 'ne'],
140
                'closure' => function ($item, $property, $value) {
141
                    return $item[$property] != $value[0];
142
                },
143
            ],
144
145
            [
146
                'tokens' => ['!==', 'ni'],
147
                'closure' => function ($item, $property, $value) {
148
                    return $item[$property] !== $value[0];
149
                },
150
            ],
151
152
            [
153
                'tokens' => ['<', 'lt'],
154
                'closure' => function ($item, $property, $value) {
155
                    return $item[$property] < $value[0];
156
                },
157
            ],
158
159
            [
160
                'tokens' => ['>', 'gt'],
161
                'closure' => function ($item, $property, $value) {
162
                    return $item[$property] > $value[0];
163
                },
164
            ],
165
166
            [
167
                'tokens' => ['<=', 'lte'],
168
                'closure' => function ($item, $property, $value) {
169
                    return $item[$property] <= $value[0];
170
                },
171
            ],
172
173
            [
174
                'tokens' => ['>=', 'gte'],
175
                'closure' => function ($item, $property, $value) {
176
                    return $item[$property] >= $value[0];
177
                },
178
            ],
179
180
            [
181
                'tokens' => ['in', 'contains'],
182
                'closure' => function ($item, $property, $value) {
183
                    return \in_array($item[$property], (array) $value, true);
184
                },
185
            ],
186
187
            [
188
                'tokens' => ['not-in', 'not-contains'],
189
                'closure' => function ($item, $property, $value) {
190
                    return !\in_array($item[$property], (array) $value, true);
191
                },
192
            ],
193
194
            [
195
                'tokens' => ['between'],
196
                'closure' => function ($item, $property, $value) {
197
                    return ($item[$property] >= $value[0] && $item[$property] <= $value[1]);
198
                },
199
            ],
200
201
            [
202
                'tokens' => ['not-between'],
203
                'closure' => function ($item, $property, $value) {
204
                    return ($item[$property] < $value[0] || $item[$property] > $value[1]);
205
                },
206
            ],
207
        ];
208
    }
209
210
211
    /**
212
     * DotArray Constructor.
213
     *
214
     * @param mixed $items
215
     */
216
    public function __construct($items = [])
217
    {
218
        $this->items = static::normalize($items);
219
    }
220
221
222
    /**
223
     * DotArray Destructor.
224
     */
225
    public function __destruct()
226
    {
227
        unset($this->uniqueIdentifier);
228
        unset($this->items);
229
        unset($this->nestedPathPattern);
230
    }
231
232
233
    /**
234
     * Call object as function.
235
     *
236
     * @param null|string $key
237
     *
238
     * @return mixed
239
     */
240
    public function __invoke($key = null)
241
    {
242
        return $this->get($key);
243
    }
244
245
246
    /**
247
     * @return string
248
     */
249
    public function uniqueIdentifier()
250
    {
251
        if (empty($this->uniqueIdentifier)) {
252
            $this->uniqueIdentifier = vsprintf(
253
                '{%s}.{%s}.{%s}',
254
                [
255
                    static::class,
256
                    \uniqid('', true),
257
                    microtime(true),
258
                ]
259
            );
260
        }
261
262
        return $this->uniqueIdentifier;
263
    }
264
265
266
    /**
267
     * Getting the nested path pattern.
268
     *
269
     * @return string
270
     */
271
    protected function nestedPathPattern()
272
    {
273
        if (empty($this->nestedPathPattern)) {
274
            $path = $this->config['path']['template'];
275
276
            foreach ($this->config['path']['wildcards'] as $wildcard => $tokens) {
277
                $path = \str_replace($wildcard, \implode('', $tokens), $path);
278
            }
279
280
            $this->nestedPathPattern = $path;
281
        }
282
283
        return $this->nestedPathPattern;
284
    }
285
286
287
    /**
288
     * Converts dot string path to segments.
289
     *
290
     * @param string $path
291
     *
292
     * @return array
293
     */
294
    protected function pathToSegments($path)
295
    {
296
        $path     = \trim($path, " \t\n\r\0\x0B\.");
297
        $segments = [];
298
        $matches  = [];
299
300
        \preg_match_all($this->nestedPathPattern(), $path, $matches);
301
302
        if (!empty($matches[1])) {
303
            $matches = $matches[1];
304
305
            $segments = \array_filter(
306
                $matches,
307
                function ($match) {
308
                    return (\mb_strlen($match, 'UTF-8') > 0);
309
                }
310
            );
311
        }
312
313
        unset($matches);
314
315
        return (empty($segments) ? [] : $segments);
316
    }
317
318
319
    /**
320
     * Wrap a given string into special characters.
321
     *
322
     * @param string $key
323
     *
324
     * @return string
325
     */
326
    protected function wrapSegmentKey($key)
327
    {
328
        return "{{~$key~}}";
329
    }
330
331
332
    /**
333
     * @param array $segments
334
     *
335
     * @return string
336
     */
337
    protected function segmentsToKey(array $segments)
338
    {
339
        return (
340
            \implode(
341
                '',
342
                \array_map(
343
                    [$this, 'wrapSegmentKey'],
344
                    $segments
345
                )
346
            )
347
        );
348
    }
349
350
351
    /**
352
     * @param array|DotArray|mixed      $array1
353
     * @param null|array|DotArray|mixed $array2
354
     *
355
     * @return array
356
     */
357
    protected static function mergeRecursive($array1, $array2 = null)
358
    {
359
        $args = \func_get_args();
360
        $res  = \array_shift($args);
361
362
        while (!empty($args)) {
363
            foreach (\array_shift($args) as $k => $v) {
364
                if ($v instanceof self) {
365
                    $v = $v->toArray();
366
                }
367
368
                if (\is_int($k)) {
369
                    if (\array_key_exists($k, $res)) {
370
                        $res[] = $v;
371
                    } else {
372
                        $res[$k] = $v;
373
                    }
374
                } else if (
375
                    \is_array($v)
376
                    && isset($res[$k])
377
                    && \is_array($res[$k])
378
                ) {
379
                    $res[$k] = static::mergeRecursive($res[$k], $v);
380
                } else {
381
                    $res[$k] = $v;
382
                }
383
            }//end foreach
384
        }//end while
385
386
        return $res;
387
    }
388
389
390
    /**
391
     * Merges one or more arrays into master recursively.
392
     * If each array has an element with the same string key value, the latter
393
     * will overwrite the former (different from array_merge_recursive).
394
     * Recursive merging will be conducted if both arrays have an element of array
395
     * type and are having the same key.
396
     * For integer-keyed elements, the elements from the latter array will
397
     * be appended to the former array.
398
     *
399
     * @param array|DotArray|mixed $array Array to be merged from. You can specify additional
400
     *                                    arrays via second argument, third argument, fourth argument etc.
401
     *
402
     * @return static
403
     */
404
    public function merge($array)
405
    {
406
        $this->items = \call_user_func_array(
407
            [
408
                $this, 'mergeRecursive',
409
            ],
410
            \array_merge(
411
                [$this->items],
412
                \func_get_args()
413
            )
414
        );
415
416
        return $this;
417
    }
418
419
420
    /**
421
     * @param string $key
422
     * @param mixed  $default
423
     *
424
     * @return array|mixed
425
     */
426
    protected function &read($key, $default)
427
    {
428
        $segments = $this->pathToSegments($key);
429
        $items    = &$this->items;
430
431
        foreach ($segments as $segment) {
432
            if (
433
                !\is_array($items)
434
                || !\array_key_exists($segment, $items)
435
            ) {
436
                return $default;
437
            }
438
439
            $items = &$items[$segment];
440
        }
441
442
        return $items;
443
    }
444
445
446
    /**
447
     * @param string $key
448
     * @param mixed  $value
449
     *
450
     * @return void
451
     */
452
    protected function write($key, $value)
453
    {
454
        $segments = $this->pathToSegments($key);
455
        $count    = \count($segments);
456
        $items    = &$this->items;
457
458
        for ($i = 0; $i < $count; $i++) {
459
            $segment = $segments[$i];
460
461
            if (
462
                (
463
                    !isset($items[$segment])
464
                    || !\is_array($items[$segment])
465
                )
466
                && ($i < ($count - 1))
467
            ) {
468
                $items[$segment] = [];
469
            }
470
471
            $items = &$items[$segment];
472
        }
473
474
        $items = $value;
475
    }
476
477
478
    /**
479
     * Delete the given key or keys.
480
     *
481
     * @param string $key
482
     *
483
     * @return void
484
     */
485
    protected function remove($key)
486
    {
487
        $segments = $this->pathToSegments($key);
488
        $count    = \count($segments);
489
        $items    = &$this->items;
490
491
        for ($i = 0; $i < $count; $i++) {
492
            $segment = $segments[$i];
493
494
            // Nothing to unset.
495
            if (!\array_key_exists($segment, $items)) {
496
                break;
497
            }
498
499
            // Last item, time to unset.
500
            if ($i === ($count - 1)) {
501
                unset($items[$segment]);
502
                break;
503
            }
504
505
            $items = &$items[$segment];
506
        }
507
    }
508
509
510
    /**
511
     * @param string $key
512
     *
513
     * @return bool
514
     */
515
    public function has($key)
516
    {
517
        return ($this->read($key, $this->uniqueIdentifier()) !== $this->uniqueIdentifier());
518
    }
519
520
521
    /**
522
     * Check if a given key is empty.
523
     *
524
     * @param null|string $key
525
     *
526
     * @return bool
527
     */
528
    public function isEmpty($key = null)
529
    {
530
        if (!isset($key)) {
531
            return empty($this->items);
532
        }
533
534
        $items = $this->read($key, null);
535
536
        if ($items instanceof self) {
537
            $items = $items->toArray();
538
        }
539
540
        return empty($items);
541
    }
542
543
544
    /**
545
     * @param null|string $key
546
     * @param null|mixed  $default
547
     *
548
     * @return mixed|static
549
     */
550
    public function get($key = null, $default = null)
551
    {
552
        $items = $this->read($key, $default);
553
554
        if (\is_array($items)) {
555
            $items = static::create($items);
556
        }
557
558
        return $items;
559
    }
560
561
562
    /**
563
     * Set the given value to the provided key or keys.
564
     *
565
     * @param string|array $keys
566
     * @param mixed        $value
567
     *
568
     * @return static
569
     */
570
    public function set($keys, $value)
571
    {
572
        $keys = (array) $keys;
573
574
        foreach ($keys as $key) {
575
            $this->write($key, $value);
576
        }
577
578
        return $this;
579
    }
580
581
582
    /**
583
     * Delete the given key or keys.
584
     *
585
     * @param string|array $keys
586
     *
587
     * @return static
588
     */
589
    public function delete($keys)
590
    {
591
        $keys = (array) $keys;
592
593
        foreach ($keys as $key) {
594
            $this->remove($key);
595
        }
596
597
        return $this;
598
    }
599
600
601
    /**
602
     * Set the contents of a given key or keys to the given value (default is empty array).
603
     *
604
     * @param null|string|array $keys
605
     * @param array             $value
606
     *
607
     * @return static
608
     */
609
    public function clear($keys = null, $value = [])
610
    {
611
        $keys = (array) (!isset($keys) ? [$keys] : $keys);
612
613
        foreach ($keys as $key) {
614
            $this->write($key, $value);
615
        }
616
617
        return $this;
618
    }
619
620
621
    /**
622
     * Find the first item in an array that passes the truth test, otherwise return false
623
     * The signature of the callable must be: `function ($value, $key)`
624
     *
625
     * @param \Closure $closure
626
     *
627
     * @return false|mixed
628
     */
629
    public function find(\Closure $closure)
630
    {
631
        foreach ($this->items as $key => $value) {
632
            if ($closure($value, $key)) {
633
                if (\is_array($value)) {
634
                    $value = static::create($value);
635
                }
636
637
                return $value;
638
            }
639
        }
640
641
        return false;
642
    }
643
644
645
    /**
646
     * Use a callable function to filter through items.
647
     * The signature of the callable must be: `function ($value, $key)`
648
     *
649
     * @param \Closure|null $closure
650
     * @param int           $flag    Flag determining what arguments are sent to callback.
651
     *                               ARRAY_FILTER_USE_KEY :: pass key as the only argument
652
     *                               to callback.
653
     *                               ARRAY_FILTER_USE_BOTH :: pass both value
654
     *                               and key as arguments to callback.
655
     *
656
     * @return static
657
     */
658
    public function filter(\Closure $closure = null, $flag = ARRAY_FILTER_USE_BOTH)
659
    {
660
        $items = $this->items;
661
662
        if (!isset($closure)) {
663
            return static::create($items);
664
        }
665
666
        return (
667
            static::create(
668
                \array_values(
669
                    \array_filter(
670
                        $items,
671
                        $closure,
672
                        $flag
673
                    )
674
                )
675
            )
676
        );
677
    }
678
679
680
    /**
681
     * Allow to filter an array using one of the following comparison operators:
682
     *  - [ =, ==, eq (equal) ]
683
     *  - [ ===, i (identical) ]
684
     *  - [ !=, ne (not equal) ]
685
     *  - [ !==, ni (not identical) ]
686
     *  - [ <, lt (less than) ]
687
     *  - [ >, gr (greater than) ]
688
     *  - [ <=, lte (less than or equal to) ]
689
     *  - [ =>, gte (greater than or equal to) ]
690
     *  - [ in, contains ]
691
     *  - [ not-in, not-contains ]
692
     *  - [ between ]
693
     *  - [ not-between ]
694
     *
695
     * @param string $property
696
     * @param string $comparisonOperator
697
     * @param mixed  $value
698
     *
699
     * @return static
700
     */
701
    public function filterBy($property, $comparisonOperator, $value)
702
    {
703
        $args  = \func_get_args();
704
        $value = (array) (array_slice($args, 2, count($args)));
705
706
        $closure   = null;
707
        $operators = static::operators();
708
709
        if (isset($value[0]) && \is_array($value[0])) {
710
            $value = $value[0];
711
        }
712
713
        foreach ($operators as $entry) {
714
            if (\in_array($comparisonOperator, $entry['tokens'])) {
715
                $closure = function ($item) use ($entry, $property, $value) {
716
                    $item = (array) $item;
717
718
                    if (!array_key_exists($property, $item)) {
719
                        return false;
720
                    }
721
722
                    return $entry['closure']($item, $property, $value);
723
                };
724
725
                break;
726
            }
727
        }
728
729
        return $this->filter($closure);
730
    }
731
732
733
    /**
734
     * Filtering through array.
735
     * The signature of the call can be:
736
     * - where([property, comparisonOperator, ...value])
737
     * - where(\Closure) :: The signature of the callable must be: `function ($value, $key)`
738
     * - where([\Closure]) :: The signature of the callable must be: `function ($value, $key)`
739
     *
740
     * Allowed comparison operators:
741
     *  - [ =, ==, eq (equal) ]
742
     *  - [ ===, i (identical) ]
743
     *  - [ !=, ne (not equal) ]
744
     *  - [ !==, ni (not identical) ]
745
     *  - [ <, lt (less than) ]
746
     *  - [ >, gr (greater than) ]
747
     *  - [ <=, lte (less than or equal to) ]
748
     *  - [ =>, gte (greater than or equal to) ]
749
     *  - [ in, contains ]
750
     *  - [ not-in, not-contains ]
751
     *  - [ between ]
752
     *  - [ not-between ]
753
     *
754
     * @param array|callable $criteria
755
     *
756
     * @return static
757
     */
758
    public function where($criteria)
759
    {
760
        $criteria = (array) $criteria;
761
762
        if (empty($criteria)) {
763
            return $this->filter();
764
        }
765
766
        $closure = \array_shift($criteria);
767
768
        if ($closure instanceof \Closure) {
769
            return $this->filter($closure);
770
        }
771
772
        $property           = $closure;
773
        $comparisonOperator = \array_shift($criteria);
774
        $value              = $criteria;
775
776
        if (isset($value[0]) && \is_array($value[0])) {
777
            $value = $value[0];
778
        }
779
780
        return $this->filterBy($property, $comparisonOperator, $value);
781
    }
782
783
784
    /**
785
     * Returning the first value from the current array.
786
     *
787
     * @return mixed
788
     */
789
    public function first()
790
    {
791
        $items = $this->items;
792
793
        return \array_shift($items);
794
    }
795
796
797
    /**
798
     * Whether a offset exists
799
     *
800
     * @link https://php.net/manual/en/arrayaccess.offsetexists.php
801
     *
802
     * @param mixed $offset An offset to check for.
803
     *
804
     * @return boolean true on success or false on failure.
805
     *
806
     * The return value will be casted to boolean if non-boolean was returned.
807
     * @since  5.0.0
808
     */
809
    public function offsetExists($offset)
810
    {
811
        return $this->has($offset);
812
    }
813
814
815
    /**
816
     * Offset to retrieve
817
     *
818
     * @link https://php.net/manual/en/arrayaccess.offsetget.php
819
     *
820
     * @param mixed $offset The offset to retrieve.
821
     *
822
     * @return mixed Can return all value types.
823
     *
824
     * @since 5.0.0
825
     */
826
    public function &offsetGet($offset)
827
    {
828
        return $this->read($offset, null);
829
    }
830
831
832
    /**
833
     * Offset to set
834
     *
835
     * @link https://php.net/manual/en/arrayaccess.offsetset.php
836
     *
837
     * @param mixed $offset
838
     * @param mixed $value
839
     *
840
     * @return void
841
     *
842
     * @since 5.0.0
843
     */
844
    public function offsetSet($offset, $value)
845
    {
846
        $this->write($offset, $value);
847
    }
848
849
850
    /**
851
     * Offset to unset
852
     *
853
     * @link https://php.net/manual/en/arrayaccess.offsetunset.php
854
     *
855
     * @param mixed $offset The offset to unset.
856
     *
857
     * @return void
858
     *
859
     * @since 5.0.0
860
     */
861
    public function offsetUnset($offset)
862
    {
863
        if (\array_key_exists($offset, $this->items)) {
864
            unset($this->items[$offset]);
865
            return;
866
        }
867
868
        $this->remove($offset);
869
    }
870
871
872
    /**
873
     * Count elements of an object
874
     *
875
     * @link https://php.net/manual/en/countable.count.php
876
     *
877
     * @param int $mode
878
     *
879
     * @return int The custom count as an integer.
880
     *
881
     * @since 5.1.0
882
     */
883
    public function count($mode = COUNT_NORMAL)
884
    {
885
        return \count($this->items, $mode);
886
    }
887
888
889
    /**
890
     * @return array
891
     */
892
    public function toArray()
893
    {
894
        return $this->items;
895
    }
896
897
898
    /**
899
     * @param int $options
900
     *
901
     * @return string
902
     */
903
    public function toJson($options = 0)
904
    {
905
        return \json_encode($this->items, $options);
906
    }
907
908
909
    /**
910
     * Specify data which should be serialized to JSON
911
     *
912
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
913
     *
914
     * @return mixed data which can be serialized by <b>json_encode</b>,
915
     * which is a value of any type other than a resource.
916
     *
917
     * @since 5.4.0
918
     */
919
    public function jsonSerialize()
920
    {
921
        return $this->items;
922
    }
923
924
925
    /**
926
     * String representation of object
927
     *
928
     * @link https://php.net/manual/en/serializable.serialize.php
929
     *
930
     * @return string the string representation of the object or null
931
     *
932
     * @since 5.1.0
933
     */
934
    public function serialize()
935
    {
936
        return \serialize($this->items);
937
    }
938
939
940
    /**
941
     * Constructs the object
942
     *
943
     * @link https://php.net/manual/en/serializable.unserialize.php
944
     *
945
     * @param string $serialized The string representation of the object.
946
947
     * @return void
948
     *
949
     * @since 5.1.0
950
     */
951
    public function unserialize($serialized)
952
    {
953
        $this->items = \unserialize($serialized);
954
    }
955
956
957
    /**
958
     * Retrieve an external iterator.
959
     *
960
     * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
961
     *
962
     * @return \ArrayIterator An instance of an object implementing Iterator or Traversable
963
     *
964
     * @since 5.0.0
965
     */
966
    public function getIterator()
967
    {
968
        return new \ArrayIterator($this->items);
969
    }
970
971
972
}
973