Passed
Pull Request — master (#62)
by Alexander
07:46
created

ArrayCollection::withModifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
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
     *
78
     * @return self
79
     */
80 23
    public function mergeWith(...$args): self
81
    {
82 23
        array_unshift($args, $this);
83
84 23
        $arrays = [];
85 23
        foreach ($args as $arg) {
86 23
            $arrays[] = $arg instanceof self ? $arg->data : $arg;
87
        }
88
89 23
        $collections = [];
90 23
        foreach ($args as $index => $arg) {
91 23
            $collection = $arg instanceof self ? $arg : new self($arg);
92 23
            foreach ($collection->getModifiers() as $modifier) {
93 16
                if ($modifier instanceof BeforeMergeModifierInterface) {
94 12
                    $collection->data = $modifier->beforeMerge($arrays, $index);
95
                }
96
            }
97 23
            $collections[$index] = $collection;
98
        }
99
100 23
        $collection = $this->merge(...$collections);
101
102 23
        foreach ($collection->getModifiers() as $modifier) {
103 16
            if ($modifier instanceof AfterMergeModifierInterface) {
104 12
                $collection->data = $modifier->afterMerge($collection->data);
105
            }
106
        }
107
108 23
        return $collection;
109
    }
110
111
    /**
112
     * @param array|self ...$args
113
     *
114
     * @return self
115
     */
116 23
    private function merge(...$args): self
117
    {
118 23
        $collection = new self();
119
120 23
        while (!empty($args)) {
121 23
            $array = array_shift($args);
122
123 23
            if ($array instanceof self) {
124 23
                $collection->modifiers = array_merge($collection->modifiers, $array->modifiers);
125 23
                $collection->data = $this->merge($collection->data, $array->data)->data;
126 23
                continue;
127
            }
128
129 23
            foreach ($array as $k => $v) {
130 21
                if (is_int($k)) {
131 10
                    if (array_key_exists($k, $collection->data)) {
132 10
                        if ($collection->data[$k] !== $v) {
133 10
                            $collection->data[] = $v;
134
                        }
135
                    } else {
136 10
                        $collection->data[$k] = $v;
137
                    }
138
                } elseif (
139 18
                    isset($collection->data[$k]) &&
140 18
                    $this->isMergeable($v) &&
141 18
                    $this->isMergeable($collection->data[$k])
142
                ) {
143 11
                    $mergedCollection = $this->merge($collection->data[$k], $v);
144 11
                    $collection->data[$k] = ($collection->data[$k] instanceof self || $v instanceof self)
145 1
                        ? $mergedCollection
146 11
                        : $mergedCollection->data;
147
                } else {
148 18
                    $collection->data[$k] = $v;
149
                }
150
            }
151
        }
152
153 23
        return $collection;
154
    }
155
156
    /**
157
     * @param mixed $value
158
     *
159
     * @return bool
160
     */
161 17
    private function isMergeable($value): bool
162
    {
163 17
        return is_array($value) || $value instanceof self;
164
    }
165
166 31
    public function toArray(): array
167
    {
168 31
        if ($this->array === null) {
169 31
            $this->array = $this->performArray($this->data);
170
171 31
            $modifiers = $this->modifiers;
172 31
            usort($modifiers, function (ModifierInterface $a, ModifierInterface $b) {
173 2
                return $b->getPriority() <=> $a->getPriority();
174 31
            });
175
176 31
            foreach ($modifiers as $modifier) {
177 23
                if ($modifier instanceof DataModifierInterface) {
178 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

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