Passed
Pull Request — master (#62)
by Sergei
01:55
created

ArrayCollection::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Arrays\Collection;
6
7
use ArrayAccess;
8
use ArrayIterator;
9
use Countable;
10
use IteratorAggregate;
11
use Yiisoft\Arrays\Collection\Modifier\ModifierInterface\AfterMergeModifierInterface;
12
use Yiisoft\Arrays\Collection\Modifier\ModifierInterface\BeforeMergeModifierInterface;
13
use Yiisoft\Arrays\Collection\Modifier\ModifierInterface\DataModifierInterface;
14
use Yiisoft\Arrays\Collection\Modifier\ModifierInterface\ModifierInterface;
15
16
use function count;
17
use function is_array;
18
19
/**
20
 * Array wrapper that allows specifying modifiers. When you get array value or whole array
21
 * from the collection modifiers are applied first so you get modified data.
22
 *
23
 * When merging collections using `ArrayHelper::merge()` or `$collection->mergeWith()` original arrays
24
 * and modifers are merged separately.
25
 */
26
final class ArrayCollection implements ArrayAccess, IteratorAggregate, Countable
27
{
28
    private array $data;
29
30
    /**
31
     * @var array|null Result array cache
32
     */
33
    private ?array $array = null;
34
35
    /**
36
     * @var ModifierInterface[]
37
     */
38
    private array $modifiers;
39
40 34
    public function __construct(array $data = [], ModifierInterface ...$modifiers)
41
    {
42 34
        $this->data = $data;
43 34
        $this->modifiers = $modifiers;
44 34
    }
45
46 2
    public function withData(array $data): self
47
    {
48 2
        $new = clone $this;
49 2
        $new->data = $data;
50 2
        return $new;
51
    }
52
53
    /**
54
     * @return ModifierInterface[]
55
     */
56 25
    public function getModifiers(): array
57
    {
58 25
        return $this->modifiers;
59
    }
60
61 8
    public function withModifiers(ModifierInterface ...$modifiers): self
62
    {
63 8
        $new = clone $this;
64 8
        $new->modifiers = $modifiers;
65 8
        return $new;
66
    }
67
68 1
    public function withAddedModifiers(ModifierInterface ...$modifiers): self
69
    {
70 1
        $new = clone $this;
71 1
        $new->modifiers = array_merge($new->modifiers, $modifiers);
72 1
        return $new;
73
    }
74
75
    /**
76
     * @param array|self ...$args
77
     * @return self
78
     */
79 23
    public function mergeWith(...$args): self
80
    {
81 23
        array_unshift($args, $this);
82
83 23
        $arrays = [];
84 23
        foreach ($args as $arg) {
85 23
            $arrays[] = $arg instanceof self ? $arg->data : $arg;
86
        }
87
88 23
        $collections = [];
89 23
        foreach ($args as $index => $arg) {
90 23
            $collection = $arg instanceof self ? $arg : new self($arg);
91 23
            foreach ($collection->getModifiers() as $modifier) {
92 16
                if ($modifier instanceof BeforeMergeModifierInterface) {
93 12
                    $collection->data = $modifier->beforeMerge($arrays, $index);
94
                }
95
            }
96 23
            $collections[$index] = $collection;
97
        }
98
99 23
        $collection = $this->merge(...$collections);
100
101 23
        foreach ($collection->getModifiers() as $modifier) {
102 16
            if ($modifier instanceof AfterMergeModifierInterface) {
103 12
                $collection->data = $modifier->afterMerge($collection->data);
104
            }
105
        }
106
107 23
        return $collection;
108
    }
109
110
    /**
111
     * @param array|self ...$args
112
     * @return self
113
     */
114 23
    private function merge(...$args): self
115
    {
116 23
        $collection = new ArrayCollection();
117
118 23
        while (!empty($args)) {
119 23
            $array = array_shift($args);
120
121 23
            if ($array instanceof ArrayCollection) {
122 23
                $collection->modifiers = array_merge($collection->modifiers, $array->modifiers);
123 23
                $collection->data = $this->merge($collection->data, $array->data)->data;
124 23
                continue;
125
            }
126
127 23
            foreach ($array as $k => $v) {
128 21
                if (is_int($k)) {
129 10
                    if (array_key_exists($k, $collection->data)) {
130 10
                        if ($collection->data[$k] !== $v) {
131 10
                            $collection->data[] = $v;
132
                        }
133
                    } else {
134 10
                        $collection->data[$k] = $v;
135
                    }
136
                } elseif (
137 18
                    isset($collection->data[$k]) &&
138 18
                    static::isMergeable($v) &&
0 ignored issues
show
Bug Best Practice introduced by
The method Yiisoft\Arrays\Collectio...llection::isMergeable() 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

138
                    static::/** @scrutinizer ignore-call */ 
139
                            isMergeable($v) &&
Loading history...
139 18
                    static::isMergeable($collection->data[$k])
140
                ) {
141 11
                    $mergedCollection = $this->merge($collection->data[$k], $v);
142 11
                    $collection->data[$k] = ($collection->data[$k] instanceof self || $v instanceof self)
143 1
                        ? $mergedCollection
144 11
                        : $mergedCollection->data;
145
                } else {
146 18
                    $collection->data[$k] = $v;
147
                }
148
            }
149
        }
150
151 23
        return $collection;
152
    }
153
154
    /**
155
     * @param mixed $value
156
     * @return bool
157
     */
158 17
    private function isMergeable($value): bool
159
    {
160 17
        return is_array($value) || $value instanceof self;
161
    }
162
163 30
    public function toArray(): array
164
    {
165 30
        if ($this->array === null) {
166 30
            $this->array = $this->performArray($this->data);
167
168 30
            foreach ($this->modifiers as $modifier) {
169 22
                if ($modifier instanceof DataModifierInterface) {
170 10
                    $this->array = $modifier->apply($this->array);
0 ignored issues
show
Bug introduced by
$this->array of type null is incompatible with the type array expected by parameter $data of Yiisoft\Arrays\Collectio...ifierInterface::apply(). ( Ignorable by Annotation )

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

170
                    $this->array = $modifier->apply(/** @scrutinizer ignore-type */ $this->array);
Loading history...
171
                }
172
            }
173
        }
174 30
        return $this->array;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->array could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
175
    }
176
177 30
    private function performArray(array $array): array
178
    {
179 30
        foreach ($array as $k => $v) {
180 28
            if ($v instanceof ArrayCollection) {
181 2
                $array[$k] = $v->toArray();
182 28
            } elseif (is_array($v)) {
183 17
                $array[$k] = $this->performArray($v);
184
            } else {
185 28
                $array[$k] = $v;
186
            }
187
        }
188 30
        return $array;
189
    }
190
191
    /**
192
     * Returns an iterator for traversing the data.
193
     * This method is required by the SPL interface {@see IteratorAggregate}.
194
     * It will be implicitly called when you use `foreach` to traverse the collection.
195
     * @return ArrayIterator an iterator for traversing the cookies in the collection.
196
     */
197 1
    public function getIterator(): ArrayIterator
198
    {
199 1
        return new ArrayIterator($this->toArray());
200
    }
201
202
    /**
203
     * Returns the number of data items.
204
     * This method is required by {@see Countable} interface.
205
     * @return int number of data elements.
206
     */
207 1
    public function count(): int
208
    {
209 1
        return count($this->toArray());
210
    }
211
212
    /**
213
     * This method is required by the interface {@see ArrayAccess}.
214
     * @param mixed $offset the offset to check on
215
     * @return bool
216
     */
217 1
    public function offsetExists($offset): bool
218
    {
219 1
        return isset($this->toArray()[$offset]);
220
    }
221
222
    /**
223
     * This method is required by the interface {@see ArrayAccess}.
224
     * @param mixed $offset the offset to retrieve element.
225
     * @return mixed the element at the offset, null if no element is found at the offset
226
     */
227 1
    public function offsetGet($offset)
228
    {
229 1
        return $this->toArray()[$offset] ?? null;
230
    }
231
232
    /**
233
     * @param mixed $offset the offset to set element
234
     * @param mixed $value the element value
235
     */
236 1
    public function offsetSet($offset, $value): void
237
    {
238 1
        throw new ArrayCollectionIsImmutableException();
239
    }
240
241
    /**
242
     * @param mixed $offset the offset to unset element
243
     */
244 1
    public function offsetUnset($offset): void
245
    {
246 1
        throw new ArrayCollectionIsImmutableException();
247
    }
248
249 10
    public function __clone()
250
    {
251 10
        $this->array = null;
252 10
    }
253
}
254