DotArray   F
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 654
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 146
dl 0
loc 654
ccs 171
cts 171
cp 1
rs 2.56
c 0
b 0
f 0
wmc 73

35 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 3 1
A createFromJson() 0 3 1
A pathPattern() 0 8 1
A wrapSegmentKey() 0 3 1
A pathToSegments() 0 26 4
A segmentsToKey() 0 10 1
A get() 0 9 2
A delete() 0 9 2
A jsonSerialize() 0 3 1
B write() 0 27 8
A normalize() 0 16 6
A first() 0 3 1
A toArray() 0 3 1
A toJson() 0 3 1
A getIterator() 0 3 1
A __construct() 0 3 1
A has() 0 5 1
A __destruct() 0 3 1
A flatten() 0 16 4
A clear() 0 9 3
A remove() 0 19 4
A read() 0 16 4
A offsetUnset() 0 8 2
B mergeRecursive() 0 21 8
A count() 0 3 1
A offsetSet() 0 3 1
A merge() 0 10 1
A __invoke() 0 3 1
A isEmpty() 0 3 1
A toFlat() 0 3 1
A set() 0 9 3
A offsetExists() 0 3 1
A serialize() 0 3 1
A unserialize() 0 3 1
A offsetGet() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DotArray often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DotArray, and based on these observations, apply Extract Interface, too.

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($flatten, self::flatten($value, \array_merge($prepend, [$key])));
172 1
                continue;
173
            }
174
175 1
            $segmentsToKey = self::segmentsToKey(\array_merge($prepend, [$key]));
176
177 1
            $flatten[$segmentsToKey] = $value;
178
        }
179
180 1
        return $flatten;
181
    }
182
183
    /**
184
     * Return the given items as an array
185
     *
186
     * @param mixed $items
187
     *
188
     * @return array
189
     */
190 24
    final protected static function normalize($items)
191
    {
192 24
        if ($items instanceof self) {
193 4
            $items = $items->toArray();
194
        }
195
196 24
        if (\is_array($items)) {
197 24
            foreach ($items as $k => $v) {
198 24
                if (\is_array($v) || $v instanceof self) {
199 24
                    $v = self::normalize($v);
200
                }
201 24
                $items[$k] = $v;
202
            }
203
        }
204
205 24
        return (array) $items;
206
    }
207
208
    /**
209
     * @param array|DotArray|mixed      $array1
210
     * @param null|array|DotArray|mixed $array2
211
     *
212
     * @return array
213
     */
214 1
    final protected static function mergeRecursive($array1, $array2 = null)
215
    {
216 1
        $args = self::normalize(\func_get_args());
217 1
        $res  = \array_shift($args);
218
219 1
        while (!empty($args)) {
220 1
            foreach (\array_shift($args) as $k => $v) {
221 1
                if (\is_int($k) && \array_key_exists($k, $res)) {
222 1
                    $res[] = $v;
223 1
                    continue;
224
                }
225
226 1
                if (\is_array($v) && isset($res[$k]) && \is_array($res[$k])) {
227 1
                    $v = self::mergeRecursive($res[$k], $v);
228
                }
229
230 1
                $res[$k] = $v;
231
            }
232
        }
233
234 1
        return $res;
235
    }
236
237
    /**
238
     * DotArray Constructor.
239
     *
240
     * @param mixed $items
241
     */
242 24
    public function __construct($items = [])
243
    {
244 24
        $this->items = self::normalize($items);
245 24
    }
246
247
    /**
248
     * DotArray Destructor.
249
     */
250 22
    public function __destruct()
251
    {
252 22
        unset($this->items);
253 22
    }
254
255
    /**
256
     * Call object as function.
257
     *
258
     * @param null|string $key
259
     *
260
     * @return mixed|static
261
     */
262 1
    public function __invoke($key = null)
263
    {
264 1
        return $this->get($key);
265
    }
266
267
    /**
268
     * Merges one or more arrays into master recursively.
269
     * If each array has an element with the same string key value, the latter
270
     * will overwrite the former (different from array_merge_recursive).
271
     * Recursive merging will be conducted if both arrays have an element of array
272
     * type and are having the same key.
273
     * For integer-keyed elements, the elements from the latter array will
274
     * be appended to the former array.
275
     *
276
     * @param array|DotArray|mixed $array Array to be merged from. You can specify additional
277
     *                                    arrays via second argument, third argument, fourth argument etc.
278
     *
279
     * @return static
280
     */
281 1
    public function merge($array)
282
    {
283 1
        $this->items = \call_user_func_array(
284
            [
285 1
                $this, 'mergeRecursive',
286
            ],
287 1
            \array_values(\array_merge([$this->items], \func_get_args()))
288
        );
289
290 1
        return $this;
291
    }
292
293
    /**
294
     * @param string|null|mixed $key
295
     * @param mixed             $default
296
     *
297
     * @return array|mixed
298
     */
299 19
    protected function &read($key = null, $default = null)
300
    {
301 19
        $segments = self::pathToSegments($key);
302 19
        $items    = &$this->items;
303
304 19
        foreach ($segments as $segment) {
305 19
            if (!\is_array($items) || !\array_key_exists($segment, $items)) {
306 2
                return $default;
307
            }
308
309 19
            $items = &$items[$segment];
310
        }
311
312 19
        unset($segments);
313
314 19
        return $items;
315
    }
316
317
    /**
318
     * @param string $key
319
     * @param mixed  $value
320
     *
321
     * @return void
322
     */
323 4
    protected function write($key, $value)
324
    {
325 4
        $segments = self::pathToSegments($key);
326 4
        $count    = \count($segments);
327 4
        $items    = &$this->items;
328
329 4
        for ($i = 0; $i < $count; $i++) {
330 4
            $segment = $segments[$i];
331
332
            if (
333 4
                (!isset($items[$segment]) || !\is_array($items[$segment]))
334 4
                && ($i < ($count - 1))
335
            ) {
336 1
                $items[$segment] = [];
337
            }
338
339 4
            $items = &$items[$segment];
340
        }
341
342 4
        if (\is_array($value) || $value instanceof self) {
343 4
            $value = self::normalize($value);
344
        }
345
346 4
        $items = $value;
347
348 4
        if (!\is_array($this->items)) {
0 ignored issues
show
introduced by
The condition is_array($this->items) is always true.
Loading history...
349 1
            $this->items = self::normalize($this->items);
350
        }
351 4
    }
352
353
    /**
354
     * Delete the given key or keys.
355
     *
356
     * @param string $key
357
     *
358
     * @return void
359
     */
360 1
    protected function remove($key)
361
    {
362 1
        $segments = self::pathToSegments($key);
363 1
        $count    = \count($segments);
364 1
        $items    = &$this->items;
365
366 1
        for ($i = 0; $i < $count; $i++) {
367 1
            $segment = $segments[$i];
368
369 1
            if (!\array_key_exists($segment, $items)) {
370 1
                break;
371
            }
372
373 1
            if ($i === ($count - 1)) {
374 1
                unset($items[$segment]);
375 1
                break;
376
            }
377
378 1
            $items = &$items[$segment];
379
        }
380 1
    }
381
382
    /**
383
     * @param string $key
384
     *
385
     * @return bool
386
     */
387 2
    public function has($key)
388
    {
389 2
        $identifier = \uniqid(static::class, true);
390
391 2
        return ($identifier !== $this->read($key, $identifier));
392
    }
393
394
    /**
395
     * Check if a given key contains empty values (null, [], 0, false)
396
     *
397
     * @param null|string $key
398
     *
399
     * @return bool
400
     */
401 1
    public function isEmpty($key = null)
402
    {
403 1
        return empty($this->read($key, null));
404
    }
405
406
    /**
407
     * @param null|string $key
408
     * @param null|mixed  $default
409
     *
410
     * @return mixed|static
411
     */
412 18
    public function get($key = null, $default = null)
413
    {
414 18
        $items = $this->read($key, $default);
415
416 18
        if (\is_array($items)) {
417 11
            $items = static::create($items);
418
        }
419
420 18
        return $items;
421
    }
422
423
    /**
424
     * Set the given value to the provided key or keys.
425
     *
426
     * @param null|string|array $keys
427
     * @param mixed|mixed       $value
428
     *
429
     * @return static
430
     */
431 2
    public function set($keys = null, $value = [])
432
    {
433 2
        $keys = (array) (!isset($keys) ? [$keys] : $keys);
434
435 2
        foreach ($keys as $key) {
436 2
            $this->write($key, $value);
437
        }
438
439 2
        return $this;
440
    }
441
442
    /**
443
     * Delete the given key or keys.
444
     *
445
     * @param string|array $keys
446
     *
447
     * @return static
448
     */
449 1
    public function delete($keys)
450
    {
451 1
        $keys = (array) $keys;
452
453 1
        foreach ($keys as $key) {
454 1
            $this->remove($key);
455
        }
456
457 1
        return $this;
458
    }
459
460
    /**
461
     * Set the contents of a given key or keys to the given value (default is empty array).
462
     *
463
     * @param null|string|array $keys
464
     * @param array|mixed       $value
465
     *
466
     * @return static
467
     */
468 1
    public function clear($keys = null, $value = [])
469
    {
470 1
        $keys = (array) (!isset($keys) ? [$keys] : $keys);
471
472 1
        foreach ($keys as $key) {
473 1
            $this->write($key, $value);
474
        }
475
476 1
        return $this;
477
    }
478
479
    /**
480
     * Returning the first value from the current array.
481
     * False otherwise, in case the list is empty.
482
     *
483
     * @return mixed
484
     */
485 1
    public function first()
486
    {
487 1
        return \reset($this->items);
488
    }
489
490
    /**
491
     * Whether a offset exists
492
     *
493
     * @link https://php.net/manual/en/arrayaccess.offsetexists.php
494
     *
495
     * @param mixed $offset An offset to check for.
496
     *
497
     * @return boolean true on success or false on failure.
498
     *
499
     * The return value will be casted to boolean if non-boolean was returned.
500
     * @since  5.0.0
501
     */
502 1
    public function offsetExists($offset)
503
    {
504 1
        return $this->has($offset);
505
    }
506
507
    /**
508
     * Offset to retrieve
509
     *
510
     * @link https://php.net/manual/en/arrayaccess.offsetget.php
511
     *
512
     * @param mixed $offset The offset to retrieve.
513
     *
514
     * @return mixed Can return all value types.
515
     *
516
     * @since 5.0.0
517
     */
518 5
    public function &offsetGet($offset)
519
    {
520 5
        return $this->read($offset, null);
521
    }
522
523
    /**
524
     * Offset to set
525
     *
526
     * @link https://php.net/manual/en/arrayaccess.offsetset.php
527
     *
528
     * @param mixed $offset
529
     * @param mixed $value
530
     *
531
     * @return void
532
     *
533
     * @since 5.0.0
534
     */
535 2
    public function offsetSet($offset, $value)
536
    {
537 2
        $this->write($offset, $value);
538 2
    }
539
540
    /**
541
     * Offset to unset
542
     *
543
     * @link https://php.net/manual/en/arrayaccess.offsetunset.php
544
     *
545
     * @param mixed $offset The offset to unset.
546
     *
547
     * @return void
548
     *
549
     * @since 5.0.0
550
     */
551 1
    public function offsetUnset($offset)
552
    {
553 1
        if (\array_key_exists($offset, $this->items)) {
554 1
            unset($this->items[$offset]);
555 1
            return;
556
        }
557
558 1
        $this->remove($offset);
559 1
    }
560
561
    /**
562
     * Count elements of an object
563
     *
564
     * @link https://php.net/manual/en/countable.count.php
565
     *
566
     * @param int $mode
567
     *
568
     * @return int The custom count as an integer.
569
     *
570
     * @since 5.1.0
571
     */
572 2
    public function count($mode = COUNT_NORMAL)
573
    {
574 2
        return \count($this->items, $mode);
575
    }
576
577
    /**
578
     * Specify data which should be serialized to JSON
579
     *
580
     * @link https://php.net/manual/en/jsonserializable.jsonserialize.php
581
     *
582
     * @return mixed data which can be serialized by <b>json_encode</b>,
583
     * which is a value of any type other than a resource.
584
     *
585
     * @since 5.4.0
586
     */
587 1
    public function jsonSerialize()
588
    {
589 1
        return $this->items;
590
    }
591
592
    /**
593
     * String representation of object
594
     *
595
     * @link https://php.net/manual/en/serializable.serialize.php
596
     *
597
     * @return string the string representation of the object or null
598
     *
599
     * @since 5.1.0
600
     */
601 1
    public function serialize()
602
    {
603 1
        return \serialize($this->items);
604
    }
605
606
    /**
607
     * Constructs the object
608
     *
609
     * @link https://php.net/manual/en/serializable.unserialize.php
610
     *
611
     * @param string $serialized The string representation of the object.
612
613
     * @return void
614
     *
615
     * @since 5.1.0
616
     */
617 1
    public function unserialize($serialized)
618
    {
619 1
        $this->items = \unserialize($serialized);
620 1
    }
621
622
    /**
623
     * Retrieve an external iterator.
624
     *
625
     * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
626
     *
627
     * @return \ArrayIterator An instance of an object implementing Iterator or Traversable
628
     *
629
     * @since 5.0.0
630
     */
631 1
    public function getIterator()
632
    {
633 1
        return new \ArrayIterator($this->items);
634
    }
635
636
    /**
637
     * Getting the internal raw array.
638
     *
639
     * @return array
640
     */
641 12
    public function toArray()
642
    {
643 12
        return $this->items;
644
    }
645
646
    /**
647
     * Getting the internal raw array as JSON.
648
     *
649
     * @param int $options
650
     *
651
     * @return string
652
     */
653 1
    public function toJson($options = 0)
654
    {
655 1
        return (string) \json_encode($this->items, $options);
656
    }
657
658
    /**
659
     * Flatten the internal array using the dot delimiter,
660
     * also the keys are wrapped inside {key} (1 x curly braces).
661
     *
662
     * @return array
663
     */
664 1
    public function toFlat()
665
    {
666 1
        return self::flatten($this->items);
667
    }
668
669
}
670