Passed
Push — master ( fcaaa7...f19bd1 )
by Smoren
02:17
created

ArrayView::count()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 1
c 2
b 0
f 1
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
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\SizeError;
10
use Smoren\ArrayView\Exceptions\ReadonlyError;
11
use Smoren\ArrayView\Exceptions\ValueError;
12
use Smoren\ArrayView\Interfaces\ArraySelectorInterface;
13
use Smoren\ArrayView\Interfaces\ArrayViewInterface;
14
use Smoren\ArrayView\Traits\ArrayViewAccessTrait;
15
use Smoren\ArrayView\Traits\ArrayViewOperationsTrait;
16
use Smoren\ArrayView\Util;
17
18
/**
19
 * Class representing a view of an array or another array view
20
 * with additional methods for filtering, mapping, and transforming the data.
21
 *
22
 * ```php
23
 * $source = [1, 2, 3, 4, 5];
24
 * $view = ArrayView::toView($source);
25
 * ```
26
 *
27
 * @template T Type of array source elements.
28
 *
29
 * @implements ArrayViewInterface<T>
30
 */
31
class ArrayView implements ArrayViewInterface
32
{
33
    /**
34
     * @use ArrayViewAccessTrait<T, string|array<int|bool>|ArrayViewInterface<int|bool>|ArraySelectorInterface>
35
     *
36
     * for array access methods.
37
     */
38
    use ArrayViewAccessTrait;
39
    /**
40
     * @use ArrayViewOperationsTrait<T, string|array<int|bool>|ArrayViewInterface<int|bool>|ArraySelectorInterface>
41
     *
42
     * for utils methods.
43
     */
44
    use ArrayViewOperationsTrait;
45
46
    /**
47
     * @var array<T>|ArrayViewInterface<T> The source array or view.
48
     */
49
    protected $source;
50
    /**
51
     * @var bool Flag indicating if the view is readonly.
52
     */
53
    protected bool $readonly;
54
    /**
55
     * @var ArrayViewInterface<T>|null The parent view of the current view.
56
     */
57
    protected ?ArrayViewInterface $parentView;
58
59
    /**
60
     * Creates an ArrayView instance from the given source array or ArrayView.
61
     *
62
     * * If the source is not an ArrayView, a new ArrayView is created with the provided source.
63
     * * If the source is an ArrayView and the `readonly` parameter is specified as `true`,
64
     * a new readonly ArrayView is created.
65
     * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned.
66
     *
67
     * ##### Example
68
     * ```php
69
     * $source = [1, 2, 3, 4, 5];
70
     * $view = ArrayView::toView($source);
71
     *
72
     * $view[0]; // 1
73
     * $view['1::2']; // [2, 4]
74
     * $view['1::2'] = [22, 44];
75
     *
76
     * $view->toArray(); // [1, 22, 3, 44, 5]
77
     * $source; // [1, 22, 3, 44, 5]
78
     * ```
79
     *
80
     * ##### Readonly example
81
     * ```php
82
     * $source = [1, 2, 3, 4, 5];
83
     * $view = ArrayView::toView($source, true);
84
     *
85
     * $view['1::2']; // [2, 4]
86
     * $view['1::2'] = [22, 44]; // throws ReadonlyError
87
     * $view[0] = 11; // throws ReadonlyError
88
     * ```
89
     *
90
     * @param array<T>|ArrayViewInterface<T> $source The source array or ArrayView to create a view from.
91
     * @param bool|null $readonly Optional flag to indicate whether the view should be readonly.
92
     *
93
     * @return ArrayViewInterface<T> An ArrayView instance based on the source array or ArrayView.
94
     *
95
     * @throws ValueError if the array is not sequential.
96
     * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view.
97
     */
98
    public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface
99
    {
100
        if (!($source instanceof ArrayViewInterface)) {
101
            return new ArrayView($source, $readonly);
102
        }
103
104
        if (!$source->isReadonly() && $readonly) {
105
            return new ArrayView($source, $readonly);
106
        }
107
108
        return $source;
109
    }
110
111
    /**
112
     * {@inheritDoc}
113
     *
114
     * ##### Example:
115
     * ```php
116
     * $source = [1, 2, 3, 4, 5];
117
     * $view = ArrayView::toUnlinkedView($source);
118
     *
119
     * $view[0]; // 1
120
     * $view['1::2']; // [2, 4]
121
     * $view['1::2'] = [22, 44];
122
     *
123
     * $view->toArray(); // [1, 22, 3, 44, 5]
124
     * $source; // [1, 2, 3, 4, 5]
125
     * ```
126
     *
127
     * ##### Readonly example:
128
     * ```php
129
     * $source = [1, 2, 3, 4, 5];
130
     * $view = ArrayView::toUnlinkedView($source, true);
131
     *
132
     * $view['1::2']; // [2, 4]
133
     * $view['1::2'] = [22, 44]; // throws ReadonlyError
134
     * $view[0] = 11; // throws ReadonlyError
135
     * ```
136
     */
137
    public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface
138
    {
139
        return static::toView($source, $readonly);
140
    }
141
142
    /**
143
     * Constructor to create a new ArrayView.
144
     *
145
     * * If the source is not an ArrayView, a new ArrayView is created with the provided source.
146
     * * If the source is an ArrayView and the `readonly` parameter is specified as `true`,
147
     * a new readonly ArrayView is created.
148
     * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned.
149
     *
150
     * @param array<T>|ArrayViewInterface<T> $source The source array or view.
151
     * @param bool|null $readonly Flag indicating if the view is readonly.
152
     *
153
     * @throws ValueError if the array is not sequential.
154
     * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view.
155
     *
156
     * @see ArrayView::toView() for creating views.
157
     */
158
    public function __construct(&$source, ?bool $readonly = null)
159
    {
160
        $this->checkSequentialArgument($source);
161
162
        $this->source = &$source;
163
        $this->readonly = $readonly ?? (($source instanceof ArrayViewInterface) ? $source->isReadonly() : false);
164
        $this->parentView = ($source instanceof ArrayViewInterface) ? $source : null;
165
166
        if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) {
167
            throw new ReadonlyError("Cannot create non-readonly view for readonly source.");
168
        }
169
    }
170
171
    /**
172
     * Returns the array representation of the view.
173
     *
174
     * ##### Example
175
     * ```php
176
     * $source = [1, 2, 3, 4, 5];
177
     * $view = ArrayView::toView($source);
178
     * $view->toArray(); // [1, 2, 3, 4, 5]
179
     * ```
180
     *
181
     * @return array<T> The array representation of the view.
182
     */
183
    public function toArray(): array
184
    {
185
        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 documented return type Smoren\ArrayView\Views\T[].
Loading history...
186
    }
187
188
    /**
189
     * Returns a subview of this view based on a selector or string slice.
190
     *
191
     * ##### Example (using selector objects)
192
     * ```php
193
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
194
     *
195
     * $subview = ArrayView::toView($source)
196
     *     ->subview(new SliceSelector('::2'))                          // [1, 3, 5, 7, 9]
197
     *     ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9]
198
     *     ->subview(new IndexListSelector([0, 1, 2]))                  // [1, 5, 7]
199
     *     ->subview(new SliceSelector('1:'));                          // [5, 7]
200
     *
201
     * $subview[':'] = [55, 77];
202
     * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10]
203
     * ```
204
     *
205
     * ##### Example (using short objects)
206
     * ```php
207
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
208
     *
209
     * $subview = ArrayView::toView($source)
210
     *     ->subview('::2')                           // [1, 3, 5, 7, 9]
211
     *     ->subview([true, false, true, true, true]) // [1, 5, 7, 9]
212
     *     ->subview([0, 1, 2])                       // [1, 5, 7]
213
     *     ->subview('1:');                           // [5, 7]
214
     *
215
     * $subview[':'] = [55, 77];
216
     * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10]
217
     * ```
218
     *
219
     * ##### Readonly example
220
     * ```php
221
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
222
     * $subview = ArrayView::toView($source)->subview('::2');
223
     *
224
     * $subview[':']; // [1, 3, 5, 7, 9]
225
     * $subview[':'] = [11, 33, 55, 77, 99]; // throws ReadonlyError
226
     * $subview[0] = [11]; // throws ReadonlyError
227
     * ```
228
     *
229
     * @template S of string|array<int|bool>|ArrayViewInterface<int|bool>|ArraySelectorInterface Selector type.
230
     *
231
     * @param S $selector The selector or string to filter the subview.
0 ignored issues
show
Bug introduced by
The type Smoren\ArrayView\Views\S 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...
232
     * @param bool|null $readonly Flag indicating if the subview should be read-only.
233
     *
234
     * @return ArrayViewInterface<T> A new view representing the subview of this view.
235
     *
236
     * @throws IndexError if the selector is IndexListSelector and some indexes are out of range.
237
     * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view.
238
     * @throws KeyError if the selector is not valid (e.g. non-sequential array).
239
     */
240
    public function subview($selector, bool $readonly = null): ArrayViewInterface
241
    {
242
        return $this->toSelector($selector)->select($this, $readonly);
243
    }
244
245
    /**
246
     * Sets new values for the elements in the view.
247
     *
248
     * ##### Example
249
     * ```php
250
     * $source = [1, 2, 3, 4, 5];
251
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
252
     *
253
     * $subview->set([11, 33, 55]);
254
     * $subview->toArray(); // [11, 33, 55]
255
     *
256
     * $source; // [11, 2, 33, 4, 55]
257
     * ```
258
     *
259
     * @param array<T>|ArrayViewInterface<T>|T $newValues The new values to set.
260
     *
261
     * @return ArrayView<T> this view.
262
     *
263
     * @throws ValueError if the $newValues is not sequential array.
264
     * @throws SizeError if size of $newValues not equals to size of the view.
265
     */
266
    public function set($newValues): self
267
    {
268
        $this->checkSequentialArgument($newValues);
269
270
        if (!\is_array($newValues) && !($newValues instanceof ArrayViewInterface)) {
0 ignored issues
show
introduced by
$newValues is always a sub-type of Smoren\ArrayView\Interfaces\ArrayViewInterface.
Loading history...
271
            $size = \count($this);
272
            for ($i = 0; $i < $size; $i++) {
273
                $this[$i] = $newValues;
274
            }
275
            return $this;
276
        }
277
278
        [$dataSize, $thisSize] = [\count($newValues), \count($this)];
279
        if ($dataSize !== $thisSize) {
280
            throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
281
        }
282
283
        $size = \count($this);
284
285
        for ($i = 0; $i < $size; $i++) {
286
            $this[$i] = $newValues[$i];
287
        }
288
289
        return $this;
290
    }
291
292
    /**
293
     * Return iterator to iterate the view elements.
294
     *
295
     * ##### Example
296
     * ```php
297
     * $source = [1, 2, 3, 4, 5];
298
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
299
     *
300
     * foreach ($subview as $item) {
301
     *     // 1, 3, 5
302
     * }
303
     *
304
     * print_r([...$subview]); // [1, 3, 5]
305
     * ```
306
     *
307
     * @return \Generator<int, T>
308
     */
309
    public function getIterator(): \Generator
310
    {
311
        $size = \count($this);
312
        for ($i = 0; $i < $size; $i++) {
313
            /** @var T $item */
314
            $item = $this[$i];
315
            yield $item;
316
        }
317
    }
318
319
    /**
320
     * Return true if view is readonly, otherwise false.
321
     *
322
     * ##### Example
323
     * ```php
324
     * $source = [1, 2, 3, 4, 5];
325
     *
326
     * $readonlyView = ArrayView::toView($source, true);
327
     * $readonlyView->isReadonly(); // true
328
     *
329
     * $readonlySubview = ArrayView::toView($source)->subview('::2', true);
330
     * $readonlySubview->isReadonly(); // true
331
     *
332
     * $view = ArrayView::toView($source);
333
     * $view->isReadonly(); // false
334
     *
335
     * $subview = ArrayView::toView($source)->subview('::2');
336
     * $subview->isReadonly(); // false
337
     * ```
338
     *
339
     * @return bool
340
     */
341
    public function isReadonly(): bool
342
    {
343
        return $this->readonly;
344
    }
345
346
    /**
347
     * Return size of the view.
348
     *
349
     * ##### Example
350
     * ```php
351
     * $source = [1, 2, 3, 4, 5];
352
     *
353
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
354
     * count($subview); // 3
355
     * ```
356
     *
357
     * @return int
358
     */
359
    public function count(): int
360
    {
361
        return $this->getParentSize();
362
    }
363
364
    /**
365
     * Get the size of the parent view or source array.
366
     *
367
     * @return int The size of the parent view or source array.
368
     */
369
    protected function getParentSize(): int
370
    {
371
        return ($this->parentView !== null)
372
            ? \count($this->parentView)
373
            : \count($this->source);
374
    }
375
376
    /**
377
     * Convert the given index to a valid index within the source array.
378
     *
379
     * @param int $i The index to convert.
380
     *
381
     * @return int The converted index within the source array.
382
     *
383
     * @throws IndexError if the index is out of range and $throwError is true.
384
     */
385
    protected function convertIndex(int $i): int
386
    {
387
        return Util::normalizeIndex($i, \count($this->source));
388
    }
389
390
    /**
391
     * Check if a numeric offset exists in the source array.
392
     *
393
     * @param numeric $offset The numeric offset to check.
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...
394
     *
395
     * @return bool Returns true if the numeric offset exists in the source, false otherwise.
396
     */
397
    private function numericOffsetExists($offset): bool
0 ignored issues
show
Unused Code introduced by
The method numericOffsetExists() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
398
    {
399
        // Non-string must be integer
400
        if (!\is_string($offset) && !\is_int($offset)) {
0 ignored issues
show
introduced by
The condition is_string($offset) is always false.
Loading history...
introduced by
The condition is_int($offset) is always false.
Loading history...
401
            return false;
402
        }
403
404
        // Numeric string must be integer
405
        if (!\is_integer($offset + 0)) {
406
            return false;
407
        }
408
409
        try {
410
            $index = $this->convertIndex(intval($offset));
411
        } catch (IndexError $e) {
412
            return false;
413
        }
414
415
        return \is_array($this->source)
416
            ? \array_key_exists($index, $this->source)
417
            : $this->source->offsetExists($index);
418
    }
419
}
420