Passed
Push — master ( e74319...b3cfef )
by Smoren
01:49
created

ArrayView::toView()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 4
nc 3
nop 2
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
class ArrayView implements ArrayViewInterface
24
{
25
    /**
26
     * @var array<T>|ArrayView<T>
27
     */
28
    protected $source;
29
    /**
30
     * @var bool
31
     */
32
    protected bool $readonly;
33
    /**
34
     * @var ArrayView<T>|null
35
     */
36
    protected ?ArrayView $parentView;
37
38
    /**
39
     * {@inheritDoc}
40
     */
41
    public static function toView(&$source, ?bool $readonly = null): ArrayView
42
    {
43
        if (!($source instanceof ArrayViewInterface)) {
44
            return new ArrayView($source, $readonly);
45
        }
46
47
        if (!$source->isReadonly() && $readonly) {
48
            return new ArrayView($source, $readonly);
49
        }
50
51
        return $source;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $source returns the type Smoren\ArrayView\Interfaces\ArrayViewInterface which includes types incompatible with the type-hinted return Smoren\ArrayView\Views\ArrayView.
Loading history...
52
    }
53
54
    /**
55
     * @param array<T>|ArrayViewInterface<T> $source
56
     * @param bool|null $readonly
57
     * @throws ReadonlyError
58
     */
59
    public function __construct(&$source, ?bool $readonly = null)
60
    {
61
        if (is_array($source) && !Util::isArraySequential($source)) {
62
            throw new ValueError('Cannot create view for non-sequential array.');
63
        }
64
65
        $this->source = &$source;
66
        $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...
67
        $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...
Documentation Bug introduced by
$source instanceof Smore...erface ? $source : null is of type Smoren\ArrayView\Interfaces\ArrayViewInterface, but the property $parentView was declared to be of type Smoren\ArrayView\Views\ArrayView. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
68
69
        if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) {
70
            throw new ReadonlyError("Cannot create non-readonly view for readonly source.");
71
        }
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77
    public function toArray(): array
78
    {
79
        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...
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85
    public function filter(callable $predicate): ArrayViewInterface
86
    {
87
        return $this->is($predicate)->select($this);
88
    }
89
90
    /**
91
     * {@inheritDoc}
92
     */
93
    public function is(callable $predicate): ArraySelectorInterface
94
    {
95
        return new MaskSelector(array_map($predicate, $this->toArray()));
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     */
101
    public function subview($selector, bool $readonly = null): ArrayViewInterface
102
    {
103
        return is_string($selector)
104
            ? (new SliceSelector($selector))->select($this, $readonly)
105
            : $selector->select($this, $readonly);
106
    }
107
108
    /**
109
     * {@inheritDoc}
110
     */
111
    public function apply(callable $mapper): self
112
    {
113
        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...
114
            $this[$i] = $mapper($this[$i], $i);
115
        }
116
        return $this;
117
    }
118
119
    /**
120
     * {@inheritDoc}
121
     */
122
    public function applyWith($data, callable $mapper): ArrayViewInterface
123
    {
124
        [$dataSize, $thisSize] = [\count($data), \count($this)];
125
        if ($dataSize !== $thisSize) {
126
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
127
        }
128
129
        $dataView = ArrayView::toView($data);
130
131
        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...
132
            $this[$i] = $mapper($this[$i], $dataView[$i], $i);
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141
    public function set($newValues): ArrayViewInterface
142
    {
143
        [$dataSize, $thisSize] = [\count($newValues), \count($this)];
144
        if ($dataSize !== $thisSize) {
145
            throw new LengthError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
146
        }
147
148
        $newValuesView = ArrayView::toView($newValues);
149
150
        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...
151
            $this[$i] = $newValuesView[$i];
152
        }
153
154
        return $this;
155
    }
156
157
    /**
158
     * @return \Generator<T>
159
     */
160
    public function getIterator(): \Generator
161
    {
162
        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...
163
            yield $this[$i];
164
        }
165
    }
166
167
    /**
168
     * @return bool
169
     */
170
    public function isReadonly(): bool
171
    {
172
        return $this->readonly;
173
    }
174
175
    /**
176
     * @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...
177
     * @return bool
178
     */
179
    public function offsetExists($offset): bool
180
    {
181
        if (\is_numeric($offset)) {
182
            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

182
            return $this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset);
Loading history...
183
        }
184
185
        if (\is_string($offset) && Slice::isSlice($offset)) {
186
            return true;
187
        }
188
189
        if ($offset instanceof ArraySelectorInterface) {
190
            return true;
191
        }
192
193
        return false;
194
    }
195
196
    /**
197
     * @param numeric|string|ArraySelectorInterface $offset
198
     * @return T|array<T>
199
     */
200
    public function offsetGet($offset)
201
    {
202
        if (\is_numeric($offset)) {
203
            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

203
            if (!$this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
204
                throw new IndexError("Index {$offset} is out of range.");
205
            }
206
            return $this->source[$this->convertIndex($offset)];
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type integer expected by parameter $i of Smoren\ArrayView\Views\ArrayView::convertIndex(). ( Ignorable by Annotation )

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

206
            return $this->source[$this->convertIndex(/** @scrutinizer ignore-type */ $offset)];
Loading history...
207
        }
208
209
        if (\is_string($offset) && Slice::isSlice($offset)) {
210
            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...
211
        }
212
213
        if ($offset instanceof ArraySelectorInterface) {
214
            return $this->subview($offset)->toArray();
215
        }
216
217
        throw new KeyError("Invalid key: \"{$offset}\".");
218
    }
219
220
    /**
221
     * @param numeric|string|ArraySelectorInterface $offset
222
     * @param T|array<T>|ArrayView<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...
223
     * @return void
224
     */
225
    public function offsetSet($offset, $value): void
226
    {
227
        if ($this->isReadonly()) {
228
            throw new ReadonlyError("Cannot modify a readonly view.");
229
        }
230
231
        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

231
        if (\is_numeric($offset) && $this->numericOffsetExists(/** @scrutinizer ignore-type */ $offset)) {
Loading history...
232
            $this->source[$this->convertIndex($offset)] = $value;
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type integer expected by parameter $i of Smoren\ArrayView\Views\ArrayView::convertIndex(). ( Ignorable by Annotation )

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

232
            $this->source[$this->convertIndex(/** @scrutinizer ignore-type */ $offset)] = $value;
Loading history...
233
            return;
234
        }
235
236
        if (\is_string($offset) && Slice::isSlice($offset)) {
237
            $this->subview(new SliceSelector($offset))->set($value);
238
            return;
239
        }
240
241
        if ($offset instanceof ArraySelectorInterface) {
242
            $this->subview($offset)->set($value);
243
            return;
244
        }
245
246
        throw new KeyError("Invalid key: \"{$offset}\".");
247
    }
248
249
    /**
250
     * @param numeric|string|ArraySelectorInterface $offset
251
     * @return void
252
     * @throws NotSupportedError
253
     */
254
    public function offsetUnset($offset): void
255
    {
256
        throw new NotSupportedError();
257
    }
258
259
    /**
260
     * @return int
261
     */
262
    public function count(): int
263
    {
264
        return $this->getParentSize();
265
    }
266
267
    protected function getParentSize(): int
268
    {
269
        return ($this->parentView !== null)
270
            ? \count($this->parentView)
271
            : \count($this->source);
272
    }
273
274
    /**
275
     * @param int $i
276
     * @return int
277
     */
278
    protected function convertIndex(int $i): int
279
    {
280
        return Util::normalizeIndex($i, \count($this->source));
281
    }
282
283
    /**
284
     * @param numeric $offset
285
     * @return bool
286
     */
287
    private function numericOffsetExists($offset): bool
288
    {
289
        try {
290
            $index = $this->convertIndex($offset);
0 ignored issues
show
Bug introduced by
$offset of type Smoren\ArrayView\Views\numeric is incompatible with the type integer expected by parameter $i of Smoren\ArrayView\Views\ArrayView::convertIndex(). ( Ignorable by Annotation )

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

290
            $index = $this->convertIndex(/** @scrutinizer ignore-type */ $offset);
Loading history...
291
        } catch (IndexError $e) {
292
            return false;
293
        }
294
        return \is_array($this->source)
295
            ? \array_key_exists($index, $this->source)
296
            : $this->source->offsetExists($index);
297
    }
298
}
299