Passed
Push — master ( 088218...d1e15d )
by Smoren
02:30 queued 43s
created

ArrayView   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 64
eloc 100
c 4
b 2
f 0
dl 0
loc 325
rs 3.28

20 Methods

Rating   Name   Duplication   Size   Complexity  
A is() 0 4 1
A toUnlinkedView() 0 3 1
A isReadonly() 0 3 1
A getIterator() 0 6 2
A offsetExists() 0 15 5
A applyWith() 0 18 3
A subview() 0 5 2
A set() 0 22 6
A toArray() 0 3 1
A apply() 0 8 2
B __construct() 0 12 8
A filter() 0 3 1
A toView() 0 11 4
A offsetUnset() 0 3 1
A count() 0 3 1
A getParentSize() 0 5 2
B numericOffsetExists() 0 14 7
B offsetGet() 0 21 7
B offsetSet() 0 30 8
A convertIndex() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ArrayView often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArrayView, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Smoren\ArrayView\Views;
6
7
use Smoren\ArrayView\Exceptions\IndexError;
8
use Smoren\ArrayView\Exceptions\KeyError;
9
use Smoren\ArrayView\Exceptions\LengthError;
10
use Smoren\ArrayView\Exceptions\NotSupportedError;
11
use Smoren\ArrayView\Exceptions\ReadonlyError;
12
use Smoren\ArrayView\Exceptions\ValueError;
13
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
14
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
15
use Smoren\ArrayView\Interfaces\MaskSelectorInterface;
16
use Smoren\ArrayView\Selectors\MaskSelector;
17
use Smoren\ArrayView\Selectors\SliceSelector;
18
use Smoren\ArrayView\Structs\Slice;
19
use Smoren\ArrayView\Util;
20
21
/**
22
 * @template T
23
 *
24
 * @implements ArrayViewInterface<T>
25
 */
26
class ArrayView implements ArrayViewInterface
27
{
28
    /**
29
     * @var array<T>|ArrayViewInterface<T>
30
     */
31
    protected $source;
32
    /**
33
     * @var bool
34
     */
35
    protected bool $readonly;
36
    /**
37
     * @var ArrayViewInterface<T>|null
38
     */
39
    protected ?ArrayViewInterface $parentView;
40
41
    /**
42
     * {@inheritDoc}
43
     */
44
    public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface
45
    {
46
        if (!($source instanceof ArrayViewInterface)) {
47
            return new ArrayView($source, $readonly);
48
        }
49
50
        if (!$source->isReadonly() && $readonly) {
51
            return new ArrayView($source, $readonly);
52
        }
53
54
        return $source;
55
    }
56
57
    /**
58
     * {@inheritDoc}
59
     */
60
    public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface
61
    {
62
        return static::toView($source, $readonly);
63
    }
64
65
    /**
66
     * @param array<T>|ArrayViewInterface<T> $source
67
     * @param bool|null $readonly
68
     * @throws ReadonlyError
69
     */
70
    public function __construct(&$source, ?bool $readonly = null)
71
    {
72
        if (is_array($source) && !Util::isArraySequential($source)) {
73
            throw new ValueError('Cannot create view for non-sequential array.');
74
        }
75
76
        $this->source = &$source;
77
        $this->readonly = $readonly ?? (($source instanceof ArrayViewInterface) ? $source->isReadonly() : false);
0 ignored issues
show
introduced by
$source is always a sub-type of Smoren\ArrayView\Interfaces\ArrayViewInterface.
Loading history...
78
        $this->parentView = ($source instanceof ArrayViewInterface) ? $source : null;
0 ignored issues
show
introduced by
$source is always a sub-type of Smoren\ArrayView\Interfaces\ArrayViewInterface.
Loading history...
79
80
        if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) {
81
            throw new ReadonlyError("Cannot create non-readonly view for readonly source.");
82
        }
83
    }
84
85
    /**
86
     * {@inheritDoc}
87
     */
88
    public function toArray(): array
89
    {
90
        return [...$this];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($this) returns the type array<integer,Smoren\ArrayView\Views\ArrayView> which is incompatible with the return type mandated by Smoren\ArrayView\Interfa...iewInterface::toArray() of Smoren\ArrayView\Interfaces\T[].

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
91
    }
92
93
    /**
94
     * {@inheritDoc}
95
     */
96
    public function filter(callable $predicate): ArrayViewInterface
97
    {
98
        return $this->is($predicate)->select($this);
99
    }
100
101
    /**
102
     * {@inheritDoc}
103
     */
104
    public function is(callable $predicate): MaskSelectorInterface
105
    {
106
        $data = $this->toArray();
107
        return new MaskSelector(array_map($predicate, $data, array_keys($data)));
108
    }
109
110
    /**
111
     * {@inheritDoc}
112
     */
113
    public function subview($selector, bool $readonly = null): ArrayViewInterface
114
    {
115
        return is_string($selector)
116
            ? (new SliceSelector($selector))->select($this, $readonly)
117
            : $selector->select($this, $readonly);
118
    }
119
120
    /**
121
     * @return ArrayView<T>
122
     *
123
     * {@inheritDoc}
124
     */
125
    public function apply(callable $mapper): self
126
    {
127
        for ($i = 0; $i < \count($this); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
128
            /** @var T $item */
129
            $item = $this[$i];
130
            $this[$i] = $mapper($item, $i);
131
        }
132
        return $this;
133
    }
134
135
    /**
136
     * @template U
137
     *
138
     * @param array<U>|ArrayViewInterface<U> $data
139
     * @param callable(T, U, int): T $mapper
140
     *
141
     * @return ArrayView<T>
142
     *
143
     * {@inheritDoc}
144
     */
145
    public function applyWith($data, callable $mapper): self
146
    {
147
        [$dataSize, $thisSize] = [\count($data), \count($this)];
148
        if ($dataSize !== $thisSize) {
149
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
150
        }
151
152
        $dataView = ArrayView::toView($data);
153
154
        for ($i = 0; $i < \count($this); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
155
            /** @var T $lhs */
156
            $lhs = $this[$i];
157
            /** @var U $rhs */
158
            $rhs = $dataView[$i];
159
            $this[$i] = $mapper($lhs, $rhs, $i);
160
        }
161
162
        return $this;
163
    }
164
165
    /**
166
     * {@inheritDoc}
167
     *
168
     * @return ArrayView<T>
169
     */
170
    public function set($newValues): self
171
    {
172
        if (!\is_array($newValues) && !($newValues instanceof ArrayViewInterface)) {
173
            for ($i = 0; $i < \count($this); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
174
                $this[$i] = $newValues;
175
            }
176
            return $this;
177
        }
178
179
        [$dataSize, $thisSize] = [\count($newValues), \count($this)];
180
        if ($dataSize !== $thisSize) {
181
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
182
        }
183
184
        $newValuesView = ArrayView::toView($newValues);
185
186
        for ($i = 0; $i < \count($this); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
187
            // @phpstan-ignore-next-line
188
            $this[$i] = $newValuesView[$i];
189
        }
190
191
        return $this;
192
    }
193
194
    /**
195
     * @return \Generator<int, T>
196
     */
197
    public function getIterator(): \Generator
198
    {
199
        for ($i = 0; $i < \count($this); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
200
            /** @var T $item */
201
            $item = $this[$i];
202
            yield $item;
203
        }
204
    }
205
206
    /**
207
     * @return bool
208
     */
209
    public function isReadonly(): bool
210
    {
211
        return $this->readonly;
212
    }
213
214
    /**
215
     * @param numeric|string|ArraySelectorInterface $offset
0 ignored issues
show
Bug introduced by
The type Smoren\ArrayView\Views\numeric was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
216
     * @return bool
217
     */
218
    public function offsetExists($offset): bool
219
    {
220
        if (\is_numeric($offset)) {
221
            return $this->numericOffsetExists($offset);
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type Smoren\ArrayView\Views\numeric expected by parameter $offset of Smoren\ArrayView\Views\A...::numericOffsetExists(). ( Ignorable by Annotation )

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

221
            return $this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset);
Loading history...
222
        }
223
224
        if (\is_string($offset) && Slice::isSlice($offset)) {
225
            return true;
226
        }
227
228
        if ($offset instanceof ArraySelectorInterface) {
229
            return true;
230
        }
231
232
        return false;
233
    }
234
235
    /**
236
     * @param numeric|string|ArraySelectorInterface $offset
237
     * @return T|array<T>
238
     */
239
    #[\ReturnTypeWillChange]
240
    public function offsetGet($offset)
241
    {
242
        /** @var mixed $offset */
243
        if (\is_numeric($offset)) {
244
            if (!$this->numericOffsetExists($offset)) {
245
                throw new IndexError("Index {$offset} is out of range.");
246
            }
247
            return $this->source[$this->convertIndex(\intval($offset))];
248
        }
249
250
        if (\is_string($offset) && Slice::isSlice($offset)) {
251
            return $this->subview(new SliceSelector($offset))->toArray();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->subview(ne...or($offset))->toArray() returns the type array<integer,Smoren\Arr...w\Views\ArraySliceView> which is incompatible with the documented return type Smoren\ArrayView\Views\T...moren\ArrayView\Views\T.
Loading history...
252
        }
253
254
        if ($offset instanceof ArraySelectorInterface) {
255
            return $this->subview($offset)->toArray();
256
        }
257
258
        $strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset);
259
        throw new KeyError("Invalid key: \"{$strOffset}\".");
260
    }
261
262
    /**
263
     * @param numeric|string|ArraySelectorInterface $offset
264
     * @param T|array<T>|ArrayViewInterface<T> $value
0 ignored issues
show
Bug introduced by
The type Smoren\ArrayView\Views\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
265
     * @return void
266
     */
267
    public function offsetSet($offset, $value): void
268
    {
269
        /** @var mixed $offset */
270
        if ($this->isReadonly()) {
271
            throw new ReadonlyError("Cannot modify a readonly view.");
272
        }
273
274
        if (\is_numeric($offset)) {
275
            if (!$this->numericOffsetExists($offset)) {
276
                throw new IndexError("Index {$offset} is out of range.");
277
            }
278
279
            // @phpstan-ignore-next-line
280
            $this->source[$this->convertIndex(\intval($offset))] = $value;
281
            return;
282
        }
283
284
        if (\is_string($offset) && Slice::isSlice($offset)) {
285
            /** @var array<T>|ArrayViewInterface<T> $value */
286
            $this->subview(new SliceSelector($offset))->set($value);
287
            return;
288
        }
289
290
        if ($offset instanceof ArraySelectorInterface) {
291
            $this->subview($offset)->set($value);
292
            return;
293
        }
294
295
        $strOffset = \is_scalar($offset) ? \strval($offset) : \gettype($offset);
296
        throw new KeyError("Invalid key: \"{$strOffset}\".");
297
    }
298
299
    /**
300
     * @param numeric|string|ArraySelectorInterface $offset
301
     * @return void
302
     * @throws NotSupportedError
303
     */
304
    public function offsetUnset($offset): void
305
    {
306
        throw new NotSupportedError();
307
    }
308
309
    /**
310
     * @return int
311
     */
312
    public function count(): int
313
    {
314
        return $this->getParentSize();
315
    }
316
317
    protected function getParentSize(): int
318
    {
319
        return ($this->parentView !== null)
320
            ? \count($this->parentView)
321
            : \count($this->source);
322
    }
323
324
    /**
325
     * @param int $i
326
     * @return int
327
     */
328
    protected function convertIndex(int $i): int
329
    {
330
        return Util::normalizeIndex($i, \count($this->source));
331
    }
332
333
    /**
334
     * @param numeric $offset
335
     * @return bool
336
     */
337
    private function numericOffsetExists($offset): bool
338
    {
339
        if (!\is_string($offset) && \is_numeric($offset) && (\is_nan($offset) || \is_infinite($offset))) {
0 ignored issues
show
introduced by
The condition is_numeric($offset) is always false.
Loading history...
introduced by
The condition is_string($offset) is always false.
Loading history...
340
            return false;
341
        }
342
343
        try {
344
            $index = $this->convertIndex(intval($offset));
345
        } catch (IndexError $e) {
346
            return false;
347
        }
348
        return \is_array($this->source)
349
            ? \array_key_exists($index, $this->source)
350
            : $this->source->offsetExists($index);
351
    }
352
}
353