Passed
Push — master ( f80863...9d3e16 )
by Banciu N. Cristian Mihai
02:44
created

DotArray::dotPathPattern()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 2
nop 0
dl 0
loc 13
ccs 0
cts 7
cp 0
crap 12
rs 10
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
    /* Traits. */
22
    use DotFilteringTrait;
23
24
    /**
25
     * Internal Dot Path Config.
26
     *
27
     * @var array
28
     */
29
    protected static $dotPathConfig = [
30
        'template'  => '#(?|(?|[<token-start>](.*?)[<token-end>])|(.*?))(?:$|\.+)#i',
31
        'wrapKey'   => '{%s}',
32
        'wildcards' => [
33
            '<token-start>' => ['\'', '\"', '\[', '\(', '\{'],
34
            '<token-end>'   => ['\'', '\"', '\]', '\)', '\}'],
35
        ],
36
    ];
37
38
    /**
39
     * The cached pattern that allow to match the JSON paths that use the dot notation.
40
     *
41
     * Allowed tokens for more complex paths: '', "", [], (), {}
42
     * Examples:
43
     *
44
     * - foo.bar
45
     * - foo.'bar'
46
     * - foo."bar"
47
     * - foo.[bar]
48
     * - foo.(bar)
49
     * - foo.{bar}
50
     *
51
     * Or more complex:
52
     * - foo.{bar}.[component].{version.1.0}
53
     *
54
     * @var string
55
     */
56
    protected static $dotPathPattern;
57
58
    /**
59
     * Unique object identifier.
60
     *
61
     * @var string
62
     */
63
    protected $uniqueIdentifier;
64
65
    /**
66
     * Stores the original data.
67
     *
68
     * @var array
69
     */
70
    protected $items;
71
72
73
    /**
74
     * Creates an DotArray object.
75
     *
76
     * @param mixed $items
77
     *
78
     * @return static
79
     */
80 2
    public static function create($items)
81
    {
82 2
        return (new static($items));
83
    }
84
85
86
    /**
87
     * @param string $json
88
     *
89
     * @return static
90
     */
91 1
    public static function createFromJson($json)
92
    {
93 1
        return static::create(\json_decode($json, true));
94
    }
95
96
97
    /**
98
     * Getting the dot path pattern.
99
     *
100
     * @return string
101
     */
102
    protected static function dotPathPattern()
103
    {
104
        if (empty(self::$dotPathPattern)) {
105
            $path = self::$dotPathConfig['template'];
106
107
            foreach (self::$dotPathConfig['wildcards'] as $wildcard => $tokens) {
108
                $path = \str_replace($wildcard, \implode('', $tokens), $path);
109
            }
110
111
            self::$dotPathPattern = $path;
112
        }
113
114
        return self::$dotPathPattern;
115
    }
116
117
118
    /**
119
     * Converts dot string path to segments.
120
     *
121
     * @param string $path
122
     *
123
     * @return array
124
     */
125
    protected static function pathToSegments($path)
126
    {
127
        $path     = \trim($path, " \t\n\r\0\x0B\.");
128
        $segments = [];
129
        $matches  = [];
130
131
        if (\mb_strlen($path, 'UTF-8') === 0) {
132
            return [];
133
        }
134
135
        \preg_match_all(static::dotPathPattern(), $path, $matches);
136
137
        if (!empty($matches[1])) {
138
            $matches = $matches[1];
139
140
            $segments = \array_filter(
141
                $matches,
142
                function ($match) {
143
                    return (\mb_strlen($match, 'UTF-8') > 0);
144
                }
145
            );
146
        }
147
148
        unset($path, $matches);
149
150
        return $segments;
151
    }
152
153
154
    /**
155
     * Wrap a given string into special characters.
156
     *
157
     * @param string $key
158
     *
159
     * @return string
160
     */
161 3
    protected static function wrapSegmentKey($key)
162
    {
163 3
        return vsprintf(static::$dotPathConfig['wrapKey'], [$key]);
164
    }
165
166
167
    /**
168
     * @param array $segments
169
     *
170
     * @return string
171
     */
172 3
    protected static function segmentsToKey(array $segments)
173
    {
174
        return (
175 3
        \implode(
176 3
            '.',
177 3
            \array_map(
178
                function ($segment) {
179 3
                    return static::wrapSegmentKey($segment);
180 3
                },
181 3
                $segments
182
            )
183
        )
184
        );
185
    }
186
187
188
    /**
189
     * Flatten the internal array using the dot delimiter,
190
     * also the keys are wrapped inside {key} (1 x curly braces).
191
     *
192
     * @param array $items
193
     * @param array $prepend
194
     *
195
     * @return array
196
     */
197
    protected static function flatten(array $items, $prepend = [])
198
    {
199
        $flatten = [];
200
201
        foreach ($items as $key => $value) {
202
            if (\is_array($value) && !empty($value)) {
203
                $flatten = array_merge(
204
                    $flatten,
205
                    static::flatten(
206
                        $value,
207
                        array_merge($prepend, [$key])
208
                    )
209
                );
210
211
                continue;
212
            }
213
214
            $segmentsToKey = static::segmentsToKey(array_merge($prepend, [$key]));
215
216
            $flatten[$segmentsToKey] = $value;
217
        }
218
219
        return $flatten;
220
    }
221
222
223
    /**
224
     * Return the given items as an array
225
     *
226
     * @param mixed $items
227
     *
228
     * @return array
229
     */
230 3
    protected static function normalize($items)
231
    {
232 3
        if ($items instanceof self) {
233 1
            $items = $items->toArray();
234
        }
235
236 3
        if (\is_array($items)) {
237 3
            foreach ($items as $k => $v) {
238 3
                if (\is_array($v) || $v instanceof self) {
239 3
                    $v = static::normalize($v);
240
                }
241 3
                $items[$k] = $v;
242
            }
243
        }
244
245 3
        return (array) $items;
246
    }
247
248
249
    /**
250
     * @param array|DotArray|mixed      $array1
251
     * @param null|array|DotArray|mixed $array2
252
     *
253
     * @return array
254
     */
255
    protected static function mergeRecursive($array1, $array2 = null)
256
    {
257
        $args = static::normalize(\func_get_args());
258
        $res  = \array_shift($args);
259
260
        while (!empty($args)) {
261
            foreach (\array_shift($args) as $k => $v) {
262
                if (\is_int($k) && \array_key_exists($k, $res)) {
263
                    $res[] = $v;
264
                    continue;
265
                }
266
267
                if (\is_array($v) && isset($res[$k]) && \is_array($res[$k])) {
268
                    $v = static::mergeRecursive($res[$k], $v);
269
                }
270
271
                $res[$k] = $v;
272
            }
273
        }
274
275
        return $res;
276
    }
277
278
279
    /**
280
     * DotArray Constructor.
281
     *
282
     * @param mixed $items
283
     */
284 3
    public function __construct($items = [])
285
    {
286 3
        $this->items = static::normalize($items);
287
288 3
        $this->uniqueIdentifier();
289 3
    }
290
291
292
    /**
293
     * DotArray Destructor.
294
     */
295 2
    public function __destruct()
296
    {
297 2
        unset($this->uniqueIdentifier);
298 2
        unset($this->items);
299 2
    }
300
301
302
    /**
303
     * Call object as function.
304
     *
305
     * @param null|string $key
306
     *
307
     * @return mixed|static
308
     */
309
    public function __invoke($key = null)
310
    {
311
        return $this->get($key);
312
    }
313
314
315
    /**
316
     * @return string
317
     */
318 3
    public function uniqueIdentifier()
319
    {
320 3
        if (empty($this->uniqueIdentifier)) {
321 3
            $this->uniqueIdentifier = static::segmentsToKey(
322
                [
323 3
                    static::class,
324 3
                    \uniqid('', true),
325 3
                    \microtime(true),
326
                ]
327
            );
328
        }
329
330 3
        return $this->uniqueIdentifier;
331
    }
332
333
334
    /**
335
     * Merges one or more arrays into master recursively.
336
     * If each array has an element with the same string key value, the latter
337
     * will overwrite the former (different from array_merge_recursive).
338
     * Recursive merging will be conducted if both arrays have an element of array
339
     * type and are having the same key.
340
     * For integer-keyed elements, the elements from the latter array will
341
     * be appended to the former array.
342
     *
343
     * @param array|DotArray|mixed $array Array to be merged from. You can specify additional
344
     *                                    arrays via second argument, third argument, fourth argument etc.
345
     *
346
     * @return static
347
     */
348
    public function merge($array)
349
    {
350
        $this->items = \call_user_func_array(
351
            [
352
                $this, 'mergeRecursive',
353
            ],
354
            \array_merge(
355
                [$this->items],
356
                \func_get_args()
357
            )
358
        );
359
360
        return $this;
361
    }
362
363
364
    /**
365
     * @param string|null|mixed $key
366
     * @param mixed             $default
367
     *
368
     * @return array|mixed
369
     */
370
    protected function &read($key = null, $default = null)
371
    {
372
        $segments = static::pathToSegments($key);
373
        $items    = &$this->items;
374
375
        foreach ($segments as $segment) {
376
            if (
377
                !\is_array($items)
378
                || !\array_key_exists($segment, $items)
379
            ) {
380
                return $default;
381
            }
382
383
            $items = &$items[$segment];
384
        }
385
386
        unset($segments);
387
388
        return $items;
389
    }
390
391
392
    /**
393
     * @param string $key
394
     * @param mixed  $value
395
     *
396
     * @return void
397
     */
398
    protected function write($key, $value)
399
    {
400
        $segments = static::pathToSegments($key);
401
        $count    = \count($segments);
402
        $items    = &$this->items;
403
404
        for ($i = 0; $i < $count; $i++) {
405
            $segment = $segments[$i];
406
407
            if (
408
                (!isset($items[$segment]) || !\is_array($items[$segment]))
409
                && ($i < ($count - 1))
410
            ) {
411
                $items[$segment] = [];
412
            }
413
414
            $items = &$items[$segment];
415
        }
416
417
        if (\is_array($value) || $value instanceof self) {
418
            $value = static::normalize($value);
419
        }
420
421
        $items = $value;
422
423
        if (!\is_array($this->items)) {
0 ignored issues
show
introduced by
The condition is_array($this->items) is always true.
Loading history...
424
            $this->items = static::normalize($this->items);
425
        }
426
    }
427
428
429
    /**
430
     * Delete the given key or keys.
431
     *
432
     * @param string $key
433
     *
434
     * @return void
435
     */
436
    protected function remove($key)
437
    {
438
        $segments = static::pathToSegments($key);
439
        $count    = \count($segments);
440
        $items    = &$this->items;
441
442
        for ($i = 0; $i < $count; $i++) {
443
            $segment = $segments[$i];
444
445
            // Nothing to unset.
446
            if (!\array_key_exists($segment, $items)) {
447
                break;
448
            }
449
450
            // Last item, time to unset.
451
            if ($i === ($count - 1)) {
452
                unset($items[$segment]);
453
                break;
454
            }
455
456
            $items = &$items[$segment];
457
        }
458
    }
459
460
461
    /**
462
     * @param string $key
463
     *
464
     * @return bool
465
     */
466
    public function has($key)
467
    {
468
        $identifier = $this->uniqueIdentifier();
469
470
        return ($identifier !== $this->read($key, $identifier));
471
    }
472
473
474
    /**
475
     * Check if a given key contains empty values (null, [], 0, false)
476
     *
477
     * @param null|string $key
478
     *
479
     * @return bool
480
     */
481
    public function isEmpty($key = null)
482
    {
483
        $items = $this->read($key, null);
484
485
        return empty($items);
486
    }
487
488
489
    /**
490
     * @param null|string $key
491
     * @param null|mixed  $default
492
     *
493
     * @return mixed|static
494
     */
495
    public function get($key = null, $default = null)
496
    {
497
        $items = $this->read($key, $default);
498
499
        if (\is_array($items)) {
500
            $items = static::create($items);
501
        }
502
503
        return $items;
504
    }
505
506
507
    /**
508
     * Set the given value to the provided key or keys.
509
     *
510
     * @param null|string|array $keys
511
     * @param mixed|mixed       $value
512
     *
513
     * @return static
514
     */
515
    public function set($keys = null, $value = [])
516
    {
517
        $keys = (array) (!isset($keys) ? [$keys] : $keys);
518
519
        foreach ($keys as $key) {
520
            $this->write($key, $value);
521
        }
522
523
        return $this;
524
    }
525
526
527
    /**
528
     * Delete the given key or keys.
529
     *
530
     * @param string|array $keys
531
     *
532
     * @return static
533
     */
534
    public function delete($keys)
535
    {
536
        $keys = (array) $keys;
537
538
        foreach ($keys as $key) {
539
            $this->remove($key);
540
        }
541
542
        return $this;
543
    }
544
545
546
    /**
547
     * Set the contents of a given key or keys to the given value (default is empty array).
548
     *
549
     * @param null|string|array $keys
550
     * @param array|mixed       $value
551
     *
552
     * @return static
553
     */
554
    public function clear($keys = null, $value = [])
555
    {
556
        $keys = (array) (!isset($keys) ? [$keys] : $keys);
557
558
        foreach ($keys as $key) {
559
            $this->write($key, $value);
560
        }
561
562
        return $this;
563
    }
564
565
566
    /**
567
     * Returning the first value from the current array.
568
     *
569
     * @return mixed
570
     */
571
    public function first()
572
    {
573
        $items = $this->items;
574
575
        return \array_shift($items);
576
    }
577
578
579
    /**
580
     * Whether a offset exists
581
     *
582
     * @link https://php.net/manual/en/arrayaccess.offsetexists.php
583
     *
584
     * @param mixed $offset An offset to check for.
585
     *
586
     * @return boolean true on success or false on failure.
587
     *
588
     * The return value will be casted to boolean if non-boolean was returned.
589
     * @since  5.0.0
590
     */
591
    public function offsetExists($offset)
592
    {
593
        return $this->has($offset);
594
    }
595
596
597
    /**
598
     * Offset to retrieve
599
     *
600
     * @link https://php.net/manual/en/arrayaccess.offsetget.php
601
     *
602
     * @param mixed $offset The offset to retrieve.
603
     *
604
     * @return mixed Can return all value types.
605
     *
606
     * @since 5.0.0
607
     */
608
    public function &offsetGet($offset)
609
    {
610
        return $this->read($offset, null);
611
    }
612
613
614
    /**
615
     * Offset to set
616
     *
617
     * @link https://php.net/manual/en/arrayaccess.offsetset.php
618
     *
619
     * @param mixed $offset
620
     * @param mixed $value
621
     *
622
     * @return void
623
     *
624
     * @since 5.0.0
625
     */
626
    public function offsetSet($offset, $value)
627
    {
628
        $this->write($offset, $value);
629
    }
630
631
632
    /**
633
     * Offset to unset
634
     *
635
     * @link https://php.net/manual/en/arrayaccess.offsetunset.php
636
     *
637
     * @param mixed $offset The offset to unset.
638
     *
639
     * @return void
640
     *
641
     * @since 5.0.0
642
     */
643
    public function offsetUnset($offset)
644
    {
645
        if (\array_key_exists($offset, $this->items)) {
646
            unset($this->items[$offset]);
647
            return;
648
        }
649
650
        $this->remove($offset);
651
    }
652
653
654
    /**
655
     * Count elements of an object
656
     *
657
     * @link https://php.net/manual/en/countable.count.php
658
     *
659
     * @param int $mode
660
     *
661
     * @return int The custom count as an integer.
662
     *
663
     * @since 5.1.0
664
     */
665
    public function count($mode = COUNT_NORMAL)
666
    {
667
        return \count($this->items, $mode);
668
    }
669
670
671
    /**
672
     * Specify data which should be serialized to JSON
673
     *
674
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
675
     *
676
     * @return mixed data which can be serialized by <b>json_encode</b>,
677
     * which is a value of any type other than a resource.
678
     *
679
     * @since 5.4.0
680
     */
681 1
    public function jsonSerialize()
682
    {
683 1
        return $this->items;
684
    }
685
686
687
    /**
688
     * String representation of object
689
     *
690
     * @link https://php.net/manual/en/serializable.serialize.php
691
     *
692
     * @return string the string representation of the object or null
693
     *
694
     * @since 5.1.0
695
     */
696 1
    public function serialize()
697
    {
698 1
        return \serialize($this->items);
699
    }
700
701
702
    /**
703
     * Constructs the object
704
     *
705
     * @link https://php.net/manual/en/serializable.unserialize.php
706
     *
707
     * @param string $serialized The string representation of the object.
708
709
     * @return void
710
     *
711
     * @since 5.1.0
712
     */
713 1
    public function unserialize($serialized)
714
    {
715 1
        $this->items = \unserialize($serialized);
716 1
    }
717
718
719
    /**
720
     * Retrieve an external iterator.
721
     *
722
     * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
723
     *
724
     * @return \ArrayIterator An instance of an object implementing Iterator or Traversable
725
     *
726
     * @since 5.0.0
727
     */
728 1
    public function getIterator()
729
    {
730 1
        return new \ArrayIterator($this->items);
731
    }
732
733
734
    /**
735
     * Getting the internal raw array.
736
     *
737
     * @return array
738
     */
739 2
    public function toArray()
740
    {
741 2
        return $this->items;
742
    }
743
744
745
    /**
746
     * Getting the internal raw array as JSON.
747
     *
748
     * @param int $options
749
     *
750
     * @return string
751
     */
752 1
    public function toJson($options = 0)
753
    {
754 1
        return (string) \json_encode($this->items, $options);
755
    }
756
757
758
    /**
759
     * Flatten the internal array using the dot delimiter,
760
     * also the keys are wrapped inside {key} (1 x curly braces).
761
     *
762
     * @return array
763
     */
764
    public function toFlat()
765
    {
766
        return static::flatten($this->items);
767
    }
768
769
770
}
771