Completed
Push — master ( 234c86...6fa286 )
by Banciu N. Cristian Mihai
36:05
created

DotArray::toFlat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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