Passed
Pull Request — master (#3)
by Smoren
01:43
created

ArrayView::offsetGet()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 10
c 2
b 1
f 0
dl 0
loc 19
rs 9.2222
cc 6
nc 5
nop 1
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\Selectors\MaskSelector;
16
use Smoren\ArrayView\Selectors\SliceSelector;
17
use Smoren\ArrayView\Structs\Slice;
18
use Smoren\ArrayView\Util;
19
20
/**
21
 * @template T
22
 *
23
 * @implements ArrayViewInterface<T>
24
 */
25
class ArrayView implements ArrayViewInterface
26
{
27
    /**
28
     * @var array<T>|ArrayViewInterface<T>
29
     */
30
    protected $source;
31
    /**
32
     * @var bool
33
     */
34
    protected bool $readonly;
35
    /**
36
     * @var ArrayViewInterface<T>|null
37
     */
38
    protected ?ArrayViewInterface $parentView;
39
40
    /**
41
     * {@inheritDoc}
42
     */
43
    public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface
44
    {
45
        if (!($source instanceof ArrayViewInterface)) {
46
            return new ArrayView($source, $readonly);
47
        }
48
49
        if (!$source->isReadonly() && $readonly) {
50
            return new ArrayView($source, $readonly);
51
        }
52
53
        return $source;
54
    }
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface
60
    {
61
        return static::toView($source, $readonly);
62
    }
63
64
    /**
65
     * @param array<T>|ArrayViewInterface<T> $source
66
     * @param bool|null $readonly
67
     * @throws ReadonlyError
68
     */
69
    public function __construct(&$source, ?bool $readonly = null)
70
    {
71
        if (is_array($source) && !Util::isArraySequential($source)) {
72
            throw new ValueError('Cannot create view for non-sequential array.');
73
        }
74
75
        $this->source = &$source;
76
        $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...
77
        $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...
78
79
        if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) {
80
            throw new ReadonlyError("Cannot create non-readonly view for readonly source.");
81
        }
82
    }
83
84
    /**
85
     * {@inheritDoc}
86
     */
87
    public function toArray(): array
88
    {
89
        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...
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function filter(callable $predicate): ArrayViewInterface
96
    {
97
        return $this->is($predicate)->select($this);
98
    }
99
100
    /**
101
     * {@inheritDoc}
102
     */
103
    public function is(callable $predicate): ArraySelectorInterface
104
    {
105
        return new MaskSelector(array_map($predicate, $this->toArray()));
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     */
111
    public function subview($selector, bool $readonly = null): ArrayViewInterface
112
    {
113
        return is_string($selector)
114
            ? (new SliceSelector($selector))->select($this, $readonly)
115
            : $selector->select($this, $readonly);
116
    }
117
118
    /**
119
     * @return ArrayView<T>
120
     *
121
     * {@inheritDoc}
122
     */
123
    public function apply(callable $mapper): self
124
    {
125
        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...
126
            /** @var T $item */
127
            $item = $this[$i];
128
            $this[$i] = $mapper($item, $i);
129
        }
130
        return $this;
131
    }
132
133
    /**
134
     * @template U
135
     *
136
     * @param array<U>|ArrayViewInterface<U> $data
137
     * @param callable(T, U, int): T $mapper
138
     *
139
     * @return ArrayView<T>
140
     *
141
     * {@inheritDoc}
142
     */
143
    public function applyWith($data, callable $mapper): self
144
    {
145
        [$dataSize, $thisSize] = [\count($data), \count($this)];
146
        if ($dataSize !== $thisSize) {
147
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
148
        }
149
150
        $dataView = ArrayView::toView($data);
151
152
        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...
153
            /** @var T $lhs */
154
            $lhs = $this[$i];
155
            /** @var U $rhs */
156
            $rhs = $dataView[$i];
157
            $this[$i] = $mapper($lhs, $rhs, $i);
158
        }
159
160
        return $this;
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     *
166
     * @return ArrayView<T>
167
     */
168
    public function set($newValues): self
169
    {
170
        if (!\is_array($newValues) && !($newValues instanceof ArrayViewInterface)) {
171
            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...
172
                $this[$i] = $newValues;
173
            }
174
            return $this;
175
        }
176
177
        [$dataSize, $thisSize] = [\count($newValues), \count($this)];
178
        if ($dataSize !== $thisSize) {
179
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
180
        }
181
182
        $newValuesView = ArrayView::toView($newValues);
183
184
        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...
185
            // @phpstan-ignore-next-line
186
            $this[$i] = $newValuesView[$i];
187
        }
188
189
        return $this;
190
    }
191
192
    /**
193
     * @return \Generator<int, T>
194
     */
195
    public function getIterator(): \Generator
196
    {
197
        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...
198
            /** @var T $item */
199
            $item = $this[$i];
200
            yield $item;
201
        }
202
    }
203
204
    /**
205
     * @return bool
206
     */
207
    public function isReadonly(): bool
208
    {
209
        return $this->readonly;
210
    }
211
212
    /**
213
     * @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...
214
     * @return bool
215
     */
216
    public function offsetExists($offset): bool
217
    {
218
        if (\is_numeric($offset)) {
219
            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

219
            return $this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset);
Loading history...
220
        }
221
222
        if (\is_string($offset) && Slice::isSlice($offset)) {
223
            return true;
224
        }
225
226
        if ($offset instanceof ArraySelectorInterface) {
227
            return true;
228
        }
229
230
        return false;
231
    }
232
233
    /**
234
     * @param numeric|string|ArraySelectorInterface $offset
235
     * @return T|array<T>
236
     */
237
    #[\ReturnTypeWillChange]
238
    public function offsetGet($offset)
239
    {
240
        if (\is_numeric($offset)) {
241
            if (!$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

241
            if (!$this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
242
                throw new IndexError("Index {$offset} is out of range.");
243
            }
244
            return $this->source[$this->convertIndex(\intval($offset))];
245
        }
246
247
        if (\is_string($offset) && Slice::isSlice($offset)) {
248
            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...
249
        }
250
251
        if ($offset instanceof ArraySelectorInterface) {
252
            return $this->subview($offset)->toArray();
253
        }
254
255
        throw new KeyError("Invalid key: \"{$offset}\".");
256
    }
257
258
    /**
259
     * @param numeric|string|ArraySelectorInterface $offset
260
     * @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...
261
     * @return void
262
     */
263
    public function offsetSet($offset, $value): void
264
    {
265
        if ($this->isReadonly()) {
266
            throw new ReadonlyError("Cannot modify a readonly view.");
267
        }
268
269
        if (\is_numeric($offset) && $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

269
        if (\is_numeric($offset) && $this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
270
            // @phpstan-ignore-next-line
271
            $this->source[$this->convertIndex(\intval($offset))] = $value;
272
            return;
273
        }
274
275
        if (\is_string($offset) && Slice::isSlice($offset)) {
276
            /** @var array<T>|ArrayViewInterface<T> $value */
277
            $this->subview(new SliceSelector($offset))->set($value);
278
            return;
279
        }
280
281
        if ($offset instanceof ArraySelectorInterface) {
282
            $this->subview($offset)->set($value);
283
            return;
284
        }
285
286
        throw new KeyError("Invalid key: \"{$offset}\".");
287
    }
288
289
    /**
290
     * @param numeric|string|ArraySelectorInterface $offset
291
     * @return void
292
     * @throws NotSupportedError
293
     */
294
    public function offsetUnset($offset): void
295
    {
296
        throw new NotSupportedError();
297
    }
298
299
    /**
300
     * @return int
301
     */
302
    public function count(): int
303
    {
304
        return $this->getParentSize();
305
    }
306
307
    protected function getParentSize(): int
308
    {
309
        return ($this->parentView !== null)
310
            ? \count($this->parentView)
311
            : \count($this->source);
312
    }
313
314
    /**
315
     * @param int $i
316
     * @return int
317
     */
318
    protected function convertIndex(int $i): int
319
    {
320
        return Util::normalizeIndex($i, \count($this->source));
321
    }
322
323
    /**
324
     * @param numeric $offset
325
     * @return bool
326
     */
327
    private function numericOffsetExists($offset): bool
328
    {
329
        try {
330
            $index = $this->convertIndex(intval($offset));
331
        } catch (IndexError $e) {
332
            return false;
333
        }
334
        return \is_array($this->source)
335
            ? \array_key_exists($index, $this->source)
336
            : $this->source->offsetExists($index);
337
    }
338
}
339