Passed
Push — master ( c1d09e...501781 )
by Banciu N. Cristian Mihai
36:57
created

DotArray::delete()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace BinaryCube\DotArray;
4
5
/**
6
 * DotArray - PHP
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
     * DotArray Constructor.
118
     *
119
     * @param mixed $items
120
     */
121
    public function __construct($items = [])
122
    {
123
        $this->items = static::normalize($items);
124
    }
125
126
127
    /**
128
     * DotArray Destructor.
129
     */
130
    public function __destruct()
131
    {
132
        unset($this->uniqueIdentifier);
133
        unset($this->items);
134
        unset($this->nestedPathPattern);
135
    }
136
137
138
    /**
139
     * Call object as function.
140
     *
141
     * @param null|string $key
142
     *
143
     * @return mixed
144
     */
145
    public function __invoke($key = null)
146
    {
147
        return $this->get($key);
148
    }
149
150
151
    /**
152
     * @return string
153
     */
154
    public function uniqueIdentifier()
155
    {
156
        if (empty($this->uniqueIdentifier)) {
157
            $this->uniqueIdentifier = vsprintf(
158
                '{%s}.{%s}.{%s}',
159
                [
160
                    static::class,
161
                    \uniqid('', true),
162
                    microtime(true),
163
                ]
164
            );
165
        }
166
167
        return $this->uniqueIdentifier;
168
    }
169
170
171
    /**
172
     * Getting the nested path pattern.
173
     *
174
     * @return string
175
     */
176
    protected function nestedPathPattern()
177
    {
178
        if (empty($this->nestedPathPattern)) {
179
            $path = $this->config['path']['template'];
180
181
            foreach ($this->config['path']['wildcards'] as $wildcard => $tokens) {
182
                $path = \str_replace($wildcard, \implode('', $tokens), $path);
183
            }
184
185
            $this->nestedPathPattern = $path;
186
        }
187
188
        return $this->nestedPathPattern;
189
    }
190
191
192
    /**
193
     * Converts dot string path to segments.
194
     *
195
     * @param string $path
196
     *
197
     * @return array
198
     */
199
    protected function pathToSegments($path)
200
    {
201
        $path     = \trim($path, " \t\n\r\0\x0B\.");
202
        $segments = [];
203
        $matches  = [];
204
205
        \preg_match_all($this->nestedPathPattern(), $path, $matches);
206
207
        if (!empty($matches[1])) {
208
            $matches = $matches[1];
209
210
            $segments = \array_filter(
211
                $matches,
212
                function ($match) {
213
                    return (\mb_strlen($match, 'UTF-8') > 0);
214
                }
215
            );
216
        }
217
218
        unset($matches);
219
220
        return (empty($segments) ? [] : $segments);
221
    }
222
223
224
    /**
225
     * Wrap a given string into special characters.
226
     *
227
     * @param string $key
228
     *
229
     * @return string
230
     */
231
    protected function wrapSegmentKey($key)
232
    {
233
        return "{{~$key~}}";
234
    }
235
236
237
    /**
238
     * @param array $segments
239
     *
240
     * @return string
241
     */
242
    protected function segmentsToKey(array $segments)
243
    {
244
        return (
245
            \implode(
246
                '',
247
                \array_map(
248
                    [$this, 'wrapSegmentKey'],
249
                    $segments
250
                )
251
            )
252
        );
253
    }
254
255
256
    /**
257
     * @param array|DotArray|mixed $a
258
     * @param array|DotArray|mixed $b
259
     *
260
     * @return array
261
     */
262
    protected function mergeRecursive($a, $b)
263
    {
264
        $args = \func_get_args();
265
        $res  = \array_shift($args);
266
267
        while (!empty($args)) {
268
            foreach (\array_shift($args) as $k => $v) {
269
                if ($v instanceof self) {
270
                    $v = $v->toArray();
271
                }
272
273
                if (\is_int($k)) {
274
                    if (\array_key_exists($k, $res)) {
275
                        $res[] = $v;
276
                    } else {
277
                        $res[$k] = $v;
278
                    }
279
                } else if (
280
                    \is_array($v)
281
                    && isset($res[$k])
282
                    && \is_array($res[$k])
283
                ) {
284
                    $res[$k] = static::mergeRecursive($res[$k], $v);
0 ignored issues
show
Bug Best Practice introduced by
The method BinaryCube\DotArray\DotArray::mergeRecursive() is not static, but was called statically. ( Ignorable by Annotation )

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

284
                    /** @scrutinizer ignore-call */ 
285
                    $res[$k] = static::mergeRecursive($res[$k], $v);
Loading history...
285
                } else {
286
                    $res[$k] = $v;
287
                }
288
            }//end foreach
289
        }//end while
290
291
        return $res;
292
    }
293
294
295
    /**
296
     * Merges one or more arrays into master recursively.
297
     * If each array has an element with the same string key value, the latter
298
     * will overwrite the former (different from array_merge_recursive).
299
     * Recursive merging will be conducted if both arrays have an element of array
300
     * type and are having the same key.
301
     * For integer-keyed elements, the elements from the latter array will
302
     * be appended to the former array.
303
     *
304
     * @param array|DotArray|mixed $a Array to be merged from. You can specify additional
305
     *                                arrays via third argument, fourth argument etc.
306
     *
307
     * @return static
308
     */
309
    public function merge($a)
310
    {
311
        $this->items = \call_user_func_array(
312
            [
313
                $this, 'mergeRecursive',
314
            ],
315
            \array_merge(
316
                [$this->items],
317
                \func_get_args()
318
            )
319
        );
320
321
        return $this;
322
    }
323
324
325
    /**
326
     * @param string $key
327
     * @param mixed  $default
328
     *
329
     * @return array|mixed
330
     */
331
    protected function &read($key, $default)
332
    {
333
        $segments = $this->pathToSegments($key);
334
        $items    = &$this->items;
335
336
        foreach ($segments as $segment) {
337
            if (
338
                !\is_array($items)
339
                || !\array_key_exists($segment, $items)
340
            ) {
341
                return $default;
342
            }
343
344
            $items = &$items[$segment];
345
        }
346
347
        return $items;
348
    }
349
350
351
    /**
352
     * @param string $key
353
     * @param mixed  $value
354
     *
355
     * @return void
356
     */
357
    protected function write($key, $value)
358
    {
359
        $segments = $this->pathToSegments($key);
360
        $count    = \count($segments);
361
        $items    = &$this->items;
362
363
        for ($i = 0; $i < $count; $i++) {
364
            $segment = $segments[$i];
365
366
            if (
367
                (
368
                    !isset($items[$segment])
369
                    || !\is_array($items[$segment])
370
                )
371
                && ($i < ($count - 1))
372
            ) {
373
                $items[$segment] = [];
374
            }
375
376
            $items = &$items[$segment];
377
        }
378
379
        $items = $value;
380
    }
381
382
383
    /**
384
     * Delete the given key or keys.
385
     *
386
     * @param string $key
387
     *
388
     * @return void
389
     */
390
    protected function remove($key)
391
    {
392
        $segments = $this->pathToSegments($key);
393
        $count    = \count($segments);
394
        $items    = &$this->items;
395
396
        for ($i = 0; $i < $count; $i++) {
397
            $segment = $segments[$i];
398
399
            // Nothing to unset.
400
            if (!\array_key_exists($segment, $items)) {
401
                break;
402
            }
403
404
            // Last item, time to unset.
405
            if ($i === ($count - 1)) {
406
                unset($items[$segment]);
407
                break;
408
            }
409
410
            $items = &$items[$segment];
411
        }
412
    }
413
414
415
    /**
416
     * @param string $key
417
     *
418
     * @return bool
419
     */
420
    public function has($key)
421
    {
422
        return ($this->read($key, $this->uniqueIdentifier()) !== $this->uniqueIdentifier());
423
    }
424
425
426
    /**
427
     * Check if a given key is empty.
428
     *
429
     * @param null|string $key
430
     *
431
     * @return bool
432
     */
433
    public function isEmpty($key = null)
434
    {
435
        if (!isset($key)) {
436
            return empty($this->items);
437
        }
438
439
        $items = $this->read($key, null);
440
441
        if ($items instanceof self) {
442
            $items = $items->toArray();
443
        }
444
445
        return empty($items);
446
    }
447
448
449
    /**
450
     * @param null|string $key
451
     * @param null        $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
452
     *
453
     * @return mixed|static
454
     */
455
    public function get($key = null, $default = null)
456
    {
457
        $items = $this->read($key, $default);
458
459
        if (\is_array($items)) {
460
            $items = static::create($items);
461
        }
462
463
        return $items;
464
    }
465
466
467
    /**
468
     * Set the given value to the provided key or keys.
469
     *
470
     * @param string|array $keys
471
     * @param mixed        $value
472
     *
473
     * @return static
474
     */
475
    public function set($keys, $value)
476
    {
477
        $keys = (array) $keys;
478
479
        foreach ($keys as $key) {
480
            $this->write($key, $value);
481
        }
482
483
        return $this;
484
    }
485
486
487
    /**
488
     * Delete the given key or keys.
489
     *
490
     * @param string|array $keys
491
     *
492
     * @return static
493
     */
494
    public function delete($keys)
495
    {
496
        $keys = (array) $keys;
497
498
        foreach ($keys as $key) {
499
            $this->remove($key);
500
        }
501
502
        return $this;
503
    }
504
505
506
    /**
507
     * Set the contents of a given key or keys to the given value (default is empty array).
508
     *
509
     * @param null|string|array $keys
510
     * @param array             $value
511
     *
512
     * @return static
513
     */
514
    public function clear($keys = null, $value = [])
515
    {
516
        if (!isset($keys)) {
517
            $this->items = [];
518
        } else {
519
            $keys = (array) $keys;
520
521
            foreach ($keys as $key) {
522
                $this->write($key, $value);
523
            }
524
        }
525
526
        return $this;
527
    }
528
529
530
    /**
531
     * Find the first item in an array that passes the truth test, otherwise return false
532
     * The signature of the callable must be: `function ($value, $key)`
533
     *
534
     * @param \Closure $closure
535
     *
536
     * @return false|mixed
537
     */
538
    public function find(\Closure $closure)
539
    {
540
        foreach ($this->items as $key => $value) {
541
            if ($closure($value, $key)) {
542
                if (\is_array($value)) {
543
                    $value = static::create($value);
544
                }
545
546
                return $value;
547
            }
548
        }
549
550
        return false;
551
    }
552
553
554
    /**
555
     * Use a callable function to filter through items.
556
     * The signature of the callable must be: `function ($value, $key)`
557
     *
558
     * @param \Closure|null $closure
559
     * @param int           $flag    Flag determining what arguments are sent to callback.
560
     *                               ARRAY_FILTER_USE_KEY :: pass key as the only argument
561
     *                               to callback.
562
     *                               ARRAY_FILTER_USE_BOTH :: pass both value
563
     *                               and key as arguments to callback.
564
     *
565
     * @return static
566
     */
567
    public function filter(\Closure $closure = null, $flag = ARRAY_FILTER_USE_BOTH)
568
    {
569
        $items = $this->items;
570
571
        if (!isset($closure)) {
572
            return static::create($items);
573
        }
574
575
        return (
576
            static::create(
577
                \array_values(
578
                    \array_filter(
579
                        $items,
580
                        $closure,
581
                        $flag
582
                    )
583
                )
584
            )
585
        );
586
    }
587
588
589
    /**
590
     * Filtering through array.
591
     * The signature of the call can be:
592
     * - where([operation, property, ...value])
593
     * - where(\Closure)
594
     * - where(\Closure)
595
     *
596
     * Allowed operations:
597
     *   [
598
     *      =, == ===, !=, !==, <, >, <=, >=,
599
     *      in, not-in, between, not-between, eq, ne, lt, gt, lte, gte, contains, not-contains
600
     *   ]
601
     *
602
     * @param array|callable $criteria
603
     *
604
     * @return static
605
     */
606
    public function where($criteria)
607
    {
608
        $closure = null;
609
610
        if (($criteria instanceof \Closure)) {
611
            $closure = $criteria;
612
        } else if (
613
            \is_array($criteria) &&
614
            !empty($criteria)
615
        ) {
616
            $closure = \array_shift($criteria);
617
618
            if (!($closure instanceof \Closure)) {
619
                $operation = empty($closure) ? 'eq' : $closure;
620
                $property  = \array_shift($criteria);
621
                $value     = (array) $criteria;
622
623
                if (\is_array($value[0])) {
624
                    $value = $value[0];
625
                }
626
627
                $filters = [
628
                    [
629
                        'tokens' => ['=', '=='],
630
                        'closure' => function ($item, $property, $value) {
631
                            return $item[$property] == $value[0];
632
                        },
633
                    ],
634
635
                    [
636
                        'tokens' => ['===', 'eq'],
637
                        'closure' => function ($item, $property, $value) {
638
                            return $item[$property] === $value[0];
639
                        },
640
                    ],
641
642
                    [
643
                        'tokens' => ['!='],
644
                        'closure' => function ($item, $property, $value) {
645
                            return $item[$property] != $value[0];
646
                        },
647
                    ],
648
649
                    [
650
                        'tokens' => ['!==', 'ne'],
651
                        'closure' => function ($item, $property, $value) {
652
                            return $item[$property] !== $value[0];
653
                        },
654
                    ],
655
656
                    [
657
                        'tokens' => ['<', 'lt'],
658
                        'closure' => function ($item, $property, $value) {
659
                            return $item[$property] < $value[0];
660
                        },
661
                    ],
662
663
                    [
664
                        'tokens' => ['>', 'gt'],
665
                        'closure' => function ($item, $property, $value) {
666
                            return $item[$property] > $value[0];
667
                        },
668
                    ],
669
670
                    [
671
                        'tokens' => ['<=', 'lte'],
672
                        'closure' => function ($item, $property, $value) {
673
                            return $item[$property] <= $value[0];
674
                        },
675
                    ],
676
677
                    [
678
                        'tokens' => ['>=', 'gte'],
679
                        'closure' => function ($item, $property, $value) {
680
                            return $item[$property] >= $value[0];
681
                        },
682
                    ],
683
684
                    [
685
                        'tokens' => ['in', 'contains'],
686
                        'closure' => function ($item, $property, $value) {
687
                            return \in_array($item[$property], (array) $value, true);
688
                        },
689
                    ],
690
691
                    [
692
                        'tokens' => ['not-in', 'not-contains'],
693
                        'closure' => function ($item, $property, $value) {
694
                            return !\in_array($item[$property], (array) $value, true);
695
                        },
696
                    ],
697
698
                    [
699
                        'tokens' => ['between'],
700
                        'closure' => function ($item, $property, $value) {
701
                            return ($item[$property] >= $value[0] && $item[$property] <= $value[1]);
702
                        },
703
                    ],
704
705
                    [
706
                        'tokens' => ['not-between'],
707
                        'closure' => function ($item, $property, $value) {
708
                            return ($item[$property] < $value[0] || $item[$property] > $value[1]);
709
                        },
710
                    ],
711
                ];
712
713
                foreach ($filters as $filter) {
714
                    // Search for operation.
715
                    if (\in_array($operation, $filter['tokens'])) {
716
                        $closure = \Closure::fromCallable(
717
                            function ($item, $key) use ($filter, $property, $value) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

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

717
                            function ($item, /** @scrutinizer ignore-unused */ $key) use ($filter, $property, $value) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
718
                                $item = (array) $item;
719
720
                                if (!array_key_exists($property, $item)) {
721
                                    return false;
722
                                }
723
724
                                return $filter['closure']($item, $property, $value);
725
                            }
726
                        );
727
728
                        break;
729
                    }//end if
730
                }//end foreach
731
            }//end if
732
        }//end if
733
734
        // Dummy closure if nothing is provided.
735
        if (empty($closure)) {
736
            $closure = \Closure::fromCallable(
737
                function ($value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

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

737
                function ($value, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

737
                function (/** @scrutinizer ignore-unused */ $value, $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
738
                    return true;
739
                }
740
            );
741
        }
742
743
        return $this->filter($closure, ARRAY_FILTER_USE_BOTH);
744
    }
745
746
747
    /**
748
     * Returning the first value from the current array.
749
     *
750
     * @return mixed
751
     */
752
    public function first()
753
    {
754
        $items = $this->items;
755
756
        return \array_shift($items);
757
    }
758
759
760
    /**
761
     * Whether a offset exists
762
     *
763
     * @link https://php.net/manual/en/arrayaccess.offsetexists.php
764
     *
765
     * @param mixed $offset An offset to check for.
766
     *
767
     * @return boolean true on success or false on failure.
768
     *
769
     * The return value will be casted to boolean if non-boolean was returned.
770
     * @since  5.0.0
771
     */
772
    public function offsetExists($offset)
773
    {
774
        return $this->has($offset);
775
    }
776
777
778
    /**
779
     * Offset to retrieve
780
     *
781
     * @link https://php.net/manual/en/arrayaccess.offsetget.php
782
     *
783
     * @param mixed $offset The offset to retrieve.
784
     *
785
     * @return mixed Can return all value types.
786
     *
787
     * @since 5.0.0
788
     */
789
    public function &offsetGet($offset)
790
    {
791
        return $this->read($offset, null);
792
    }
793
794
795
    /**
796
     * Offset to set
797
     *
798
     * @link https://php.net/manual/en/arrayaccess.offsetset.php
799
     *
800
     * @param mixed $offset
801
     * @param mixed $value
802
     *
803
     * @return void
804
     *
805
     * @since 5.0.0
806
     */
807
    public function offsetSet($offset, $value)
808
    {
809
        $this->write($offset, $value);
810
    }
811
812
813
    /**
814
     * Offset to unset
815
     *
816
     * @link https://php.net/manual/en/arrayaccess.offsetunset.php
817
     *
818
     * @param mixed $offset The offset to unset.
819
     *
820
     * @return void
821
     *
822
     * @since 5.0.0
823
     */
824
    public function offsetUnset($offset)
825
    {
826
        if (\array_key_exists($offset, $this->items)) {
827
            unset($this->items[$offset]);
828
            return;
829
        }
830
831
        $this->remove($offset);
832
    }
833
834
835
    /**
836
     * Count elements of an object
837
     *
838
     * @link https://php.net/manual/en/countable.count.php
839
     *
840
     * @param int $mode
841
     *
842
     * @return int The custom count as an integer.
843
     *
844
     * @since 5.1.0
845
     */
846
    public function count($mode = COUNT_NORMAL)
847
    {
848
        return \count($this->items, $mode);
849
    }
850
851
852
    /**
853
     * @return array
854
     */
855
    public function toArray()
856
    {
857
        return $this->items;
858
    }
859
860
861
    /**
862
     * @param int $options
863
     *
864
     * @return string
865
     */
866
    public function toJson($options = 0)
867
    {
868
        return \json_encode($this->items, $options);
869
    }
870
871
872
    /**
873
     * Specify data which should be serialized to JSON
874
     *
875
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
876
     *
877
     * @return mixed data which can be serialized by <b>json_encode</b>,
878
     * which is a value of any type other than a resource.
879
     *
880
     * @since 5.4.0
881
     */
882
    public function jsonSerialize()
883
    {
884
        return $this->items;
885
    }
886
887
888
    /**
889
     * String representation of object
890
     *
891
     * @link https://php.net/manual/en/serializable.serialize.php
892
     *
893
     * @return string the string representation of the object or null
894
     *
895
     * @since 5.1.0
896
     */
897
    public function serialize()
898
    {
899
        return \serialize($this->items);
900
    }
901
902
903
    /**
904
     * Constructs the object
905
     *
906
     * @link https://php.net/manual/en/serializable.unserialize.php
907
     *
908
     * @param string $serialized The string representation of the object.
909
910
     * @return void
911
     *
912
     * @since 5.1.0
913
     */
914
    public function unserialize($serialized)
915
    {
916
        $this->items = \unserialize($serialized);
917
    }
918
919
920
    /**
921
     * Retrieve an external iterator.
922
     *
923
     * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
924
     *
925
     * @return \ArrayIterator An instance of an object implementing Iterator or Traversable
926
     *
927
     * @since 5.0.0
928
     */
929
    public function getIterator()
930
    {
931
        return new \ArrayIterator($this->items);
932
    }
933
934
935
}
936