Passed
Push — master ( d83213...af13b7 )
by Banciu N. Cristian Mihai
03:00
created

DotArray::pathPattern()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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