Passed
Pull Request — master (#62)
by Sergei
02:11
created

ArrayCollection::getIterator()   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 modifiers 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 35
    public function __construct(array $data = [], ModifierInterface ...$modifiers)
41
    {
42 35
        $this->data = $data;
43 35
        $this->modifiers = $modifiers;
44 35
    }
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
                    $this->isMergeable($v) &&
139 18
                    $this->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 31
    public function toArray(): array
164
    {
165 31
        if ($this->array === null) {
166 31
            $this->array = $this->performArray($this->data);
167
168 31
            $modifiers = $this->modifiers;
169 31
            usort($modifiers, function (ModifierInterface $a, ModifierInterface $b) {
170 2
                return $b->getPriority() <=> $a->getPriority();
171 31
            });
172
173 31
            foreach ($modifiers as $modifier) {
174 23
                if ($modifier instanceof DataModifierInterface) {
175 11
                    $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

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