Passed
Pull Request — master (#62)
by Sergei
14:50
created

ArrayCollection::performArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 4
nc 4
nop 1
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
final class ArrayCollection implements ArrayAccess, IteratorAggregate, Countable
17
{
18
    private array $data;
19
20
    /**
21
     * @var array|null Result array cache
22
     */
23
    private ?array $array = null;
24
25
    /**
26
     * @var ModifierInterface[]
27
     */
28
    private array $modifiers;
29
30
    /**
31
     * @param array $data
32
     * @param ModifierInterface|ModifierInterface[] $modifiers
33
     */
34
    public function __construct(array $data = [], $modifiers = [])
35
    {
36
        $this->data = $data;
37
        $this->modifiers = is_array($modifiers) ? $modifiers : [$modifiers];
38
    }
39
40
    public function withData(array $data): self
41
    {
42
        $new = clone $this;
43
        $new->data = $data;
44
        return $new;
45
    }
46
47
    /**
48
     * @return ModifierInterface[]
49
     */
50
    public function getModifiers(): array
51
    {
52
        return $this->modifiers;
53
    }
54
55
    public function withModifiers(ModifierInterface ...$modifiers): self
56
    {
57
        $new = clone $this;
58
        $new->modifiers = $modifiers;
59
        return $new;
60
    }
61
62
    public function withAddedModifiers(ModifierInterface ...$modifiers): self
63
    {
64
        $new = clone $this;
65
        $new->modifiers = array_merge($new->modifiers, $modifiers);
66
        return $new;
67
    }
68
69
    /**
70
     * @param array|self ...$args
71
     * @return self
72
     */
73
    public function mergeWith(...$args): self
74
    {
75
        array_unshift($args, $this);
76
77
        $arrays = [];
78
        foreach ($args as $arg) {
79
            $arrays[] = $arg instanceof self ? $arg->data : $arg;
80
        }
81
82
        $collections = [];
83
        foreach ($args as $index => $arg) {
84
            $collection = $arg instanceof self ? $arg : new self($arg);
85
            foreach ($collection->getModifiers() as $modifier) {
86
                if ($modifier instanceof BeforeMergeModifierInterface) {
87
                    $collection->data = $modifier->beforeMerge($arrays, $index);
88
                }
89
            }
90
            $collections[$index] = $collection;
91
        }
92
93
        $collection = $this->merge(...$collections);
94
95
        foreach ($collection->getModifiers() as $modifier) {
96
            if ($modifier instanceof AfterMergeModifierInterface) {
97
                $collection->data = $modifier->afterMerge($collection->data);
98
            }
99
        }
100
101
        return $collection;
102
    }
103
104
    /**
105
     * @param array|self ...$args
106
     * @return self
107
     */
108
    private function merge(...$args): self
109
    {
110
        $collection = new ArrayCollection();
111
112
        while (!empty($args)) {
113
            $array = array_shift($args);
114
115
            if ($array instanceof ArrayCollection) {
116
                $collection->modifiers = array_merge($collection->modifiers, $array->modifiers);
117
                $collection->data = $this->merge($collection->data, $array->data)->data;
118
                continue;
119
            }
120
121
            foreach ($array as $k => $v) {
122
                if (is_int($k)) {
123
                    if (array_key_exists($k, $collection->data)) {
124
                        if ($collection->data[$k] !== $v) {
125
                            $collection->data[] = $v;
126
                        }
127
                    } else {
128
                        $collection->data[$k] = $v;
129
                    }
130
                } elseif (
131
                    isset($collection->data[$k]) &&
132
                    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

132
                    static::/** @scrutinizer ignore-call */ 
133
                            isMergeable($v) &&
Loading history...
133
                    static::isMergeable($collection->data[$k])
134
                ) {
135
                    $mergedCollection = $this->merge($collection->data[$k], $v);
136
                    $collection->data[$k] = ($collection->data[$k] instanceof self || $v instanceof self)
137
                        ? $mergedCollection
138
                        : $mergedCollection->data;
139
                } else {
140
                    $collection->data[$k] = $v;
141
                }
142
            }
143
        }
144
145
        return $collection;
146
    }
147
148
    /**
149
     * @param mixed $value
150
     * @return bool
151
     */
152
    private function isMergeable($value): bool
153
    {
154
        return is_array($value) || $value instanceof self;
155
    }
156
157
    public function toArray(): array
158
    {
159
        if ($this->array === null) {
160
            $this->array = $this->performArray($this->data);
161
162
            foreach ($this->modifiers as $modifier) {
163
                if ($modifier instanceof DataModifierInterface) {
164
                    $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

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