Passed
Push — master ( 5d4ac5...0241ad )
by Smoren
02:53 queued 36s
created

ArrayView   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 532
Duplicated Lines 0 %

Importance

Changes 8
Bugs 2 Features 2
Metric Value
wmc 42
eloc 75
c 8
b 2
f 2
dl 0
loc 532
rs 9.0399

17 Methods

Rating   Name   Duplication   Size   Complexity  
A count() 0 3 1
A is() 0 4 1
A toUnlinkedView() 0 3 1
A isReadonly() 0 3 1
A getParentSize() 0 5 2
A getIterator() 0 7 2
A numericOffsetExists() 0 21 6
A applyWith() 0 21 3
A checkSequential() 0 4 3
A subview() 0 3 1
A set() 0 25 6
A toArray() 0 3 1
A apply() 0 9 2
A convertIndex() 0 3 1
A __construct() 0 10 6
A filter() 0 3 1
A toView() 0 11 4

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\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\Interfaces\MaskSelectorInterface;
15
use Smoren\ArrayView\Selectors\MaskSelector;
16
use Smoren\ArrayView\Traits\ArrayViewAccessTrait;
17
use Smoren\ArrayView\Util;
18
19
/**
20
 * Class representing a view of an array or another array view
21
 * with additional methods for filtering, mapping, and transforming the data.
22
 *
23
 * ```php
24
 * $source = [1, 2, 3, 4, 5];
25
 * $view = ArrayView::toView($source);
26
 * ```
27
 *
28
 * @template T Type of array source elements.
29
 *
30
 * @implements ArrayViewInterface<T>
31
 */
32
class ArrayView implements ArrayViewInterface
33
{
34
    /**
35
     * @use ArrayViewAccessTrait<T, string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface>
36
     *
37
     * for array access methods.
38
     */
39
    use ArrayViewAccessTrait;
40
41
    /**
42
     * @var array<T>|ArrayViewInterface<T> The source array or view.
43
     */
44
    protected $source;
45
    /**
46
     * @var bool Flag indicating if the view is readonly.
47
     */
48
    protected bool $readonly;
49
    /**
50
     * @var ArrayViewInterface<T>|null The parent view of the current view.
51
     */
52
    protected ?ArrayViewInterface $parentView;
53
54
    /**
55
     * Creates an ArrayView instance from the given source array or ArrayView.
56
     *
57
     * * If the source is not an ArrayView, a new ArrayView is created with the provided source.
58
     * * If the source is an ArrayView and the `readonly` parameter is specified as `true`,
59
     * a new readonly ArrayView is created.
60
     * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned.
61
     *
62
     * ##### Example
63
     * ```php
64
     * $source = [1, 2, 3, 4, 5];
65
     * $view = ArrayView::toView($source);
66
     *
67
     * $view[0]; // 1
68
     * $view['1::2']; // [2, 4]
69
     * $view['1::2'] = [22, 44];
70
     *
71
     * $view->toArray(); // [1, 22, 3, 44, 5]
72
     * $source; // [1, 22, 3, 44, 5]
73
     * ```
74
     *
75
     * ##### Readonly example
76
     * ```php
77
     * $source = [1, 2, 3, 4, 5];
78
     * $view = ArrayView::toView($source, true);
79
     *
80
     * $view['1::2']; // [2, 4]
81
     * $view['1::2'] = [22, 44]; // throws ReadonlyError
82
     * $view[0] = 11; // throws ReadonlyError
83
     * ```
84
     *
85
     * @param array<T>|ArrayViewInterface<T> $source The source array or ArrayView to create a view from.
86
     * @param bool|null $readonly Optional flag to indicate whether the view should be readonly.
87
     *
88
     * @return ArrayViewInterface<T> An ArrayView instance based on the source array or ArrayView.
89
     *
90
     * @throws ValueError if the array is not sequential.
91
     * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view.
92
     */
93
    public static function toView(&$source, ?bool $readonly = null): ArrayViewInterface
94
    {
95
        if (!($source instanceof ArrayViewInterface)) {
96
            return new ArrayView($source, $readonly);
97
        }
98
99
        if (!$source->isReadonly() && $readonly) {
100
            return new ArrayView($source, $readonly);
101
        }
102
103
        return $source;
104
    }
105
106
    /**
107
     * {@inheritDoc}
108
     *
109
     * ##### Example:
110
     * ```php
111
     * $source = [1, 2, 3, 4, 5];
112
     * $view = ArrayView::toUnlinkedView($source);
113
     *
114
     * $view[0]; // 1
115
     * $view['1::2']; // [2, 4]
116
     * $view['1::2'] = [22, 44];
117
     *
118
     * $view->toArray(); // [1, 22, 3, 44, 5]
119
     * $source; // [1, 2, 3, 4, 5]
120
     * ```
121
     *
122
     * ##### Readonly example:
123
     * ```php
124
     * $source = [1, 2, 3, 4, 5];
125
     * $view = ArrayView::toUnlinkedView($source, true);
126
     *
127
     * $view['1::2']; // [2, 4]
128
     * $view['1::2'] = [22, 44]; // throws ReadonlyError
129
     * $view[0] = 11; // throws ReadonlyError
130
     * ```
131
     */
132
    public static function toUnlinkedView($source, ?bool $readonly = null): ArrayViewInterface
133
    {
134
        return static::toView($source, $readonly);
135
    }
136
137
    /**
138
     * Constructor to create a new ArrayView.
139
     *
140
     * * If the source is not an ArrayView, a new ArrayView is created with the provided source.
141
     * * If the source is an ArrayView and the `readonly` parameter is specified as `true`,
142
     * a new readonly ArrayView is created.
143
     * * If the source is an ArrayView and it is already readonly, the same ArrayView is returned.
144
     *
145
     * @param array<T>|ArrayViewInterface<T> $source The source array or view.
146
     * @param bool|null $readonly Flag indicating if the view is readonly.
147
     *
148
     * @throws ValueError if the array is not sequential.
149
     * @throws ReadonlyError if the source is readonly and trying to create a non-readonly view.
150
     *
151
     * @see ArrayView::toView() for creating views.
152
     */
153
    public function __construct(&$source, ?bool $readonly = null)
154
    {
155
        $this->checkSequential($source);
156
157
        $this->source = &$source;
158
        $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...
159
        $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...
160
161
        if (($source instanceof ArrayViewInterface) && $source->isReadonly() && !$this->isReadonly()) {
162
            throw new ReadonlyError("Cannot create non-readonly view for readonly source.");
163
        }
164
    }
165
166
    /**
167
     * Returns the array representation of the view.
168
     *
169
     * ##### Example
170
     * ```php
171
     * $source = [1, 2, 3, 4, 5];
172
     * $view = ArrayView::toView($source);
173
     * $view->toArray(); // [1, 2, 3, 4, 5]
174
     * ```
175
     *
176
     * @return array<T> The array representation of the view.
177
     */
178
    public function toArray(): array
179
    {
180
        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...
181
    }
182
183
    /**
184
     * Filters the elements in the view based on a predicate function.
185
     *
186
     * ##### Example
187
     * ```php
188
     * $source = [1, 2, 3, 4, 5, 6];
189
     * $view = ArrayView::toView($source);
190
     *
191
     * $filtered = $view->filter(fn ($x) => $x % 2 === 0);
192
     * $filtered->toArray(); // [2, 4, 6]
193
     *
194
     * $filtered[':'] = [20, 40, 60];
195
     * $filtered->toArray(); // [20, 40, 60]
196
     * $source; // [1, 20, 3, 40, 5, 60]
197
     * ```
198
     *
199
     * @param callable(T, int): bool $predicate Function that returns a boolean value for each element.
200
     *
201
     * @return ArrayMaskView<T> A new view with elements that satisfy the predicate.
202
     */
203
    public function filter(callable $predicate): ArrayViewInterface
204
    {
205
        return $this->is($predicate)->select($this);
206
    }
207
208
    /**
209
     * Checks if all elements in the view satisfy a given predicate function.
210
     *
211
     * ##### Example
212
     * ```php
213
     * $source = [1, 2, 3, 4, 5, 6];
214
     * $view = ArrayView::toView($source);
215
     *
216
     * $mask = $view->is(fn ($x) => $x % 2 === 0);
217
     * $mask->getValue(); // [false, true, false, true, false, true]
218
     *
219
     * $view->subview($mask)->toArray(); // [2, 4, 6]
220
     * $view[$mask]; // [2, 4, 6]
221
     *
222
     * $view[$mask] = [20, 40, 60];
223
     * $source; // [1, 20, 3, 40, 5, 60]
224
     * ```
225
     *
226
     * @param callable(T, int): bool $predicate Function that returns a boolean value for each element.
227
     *
228
     * @return MaskSelector Boolean mask for selecting elements that satisfy the predicate.
229
     */
230
    public function is(callable $predicate): MaskSelectorInterface
231
    {
232
        $data = $this->toArray();
233
        return new MaskSelector(array_map($predicate, $data, array_keys($data)));
234
    }
235
236
    /**
237
     * Returns a subview of this view based on a selector or string slice.
238
     *
239
     * ##### Example (using selector objects)
240
     * ```
241
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
242
     *
243
     * $subview = ArrayView::toView($source)
244
     *     ->subview(new SliceSelector('::2'))                          // [1, 3, 5, 7, 9]
245
     *     ->subview(new MaskSelector([true, false, true, true, true])) // [1, 5, 7, 9]
246
     *     ->subview(new IndexListSelector([0, 1, 2]))                  // [1, 5, 7]
247
     *     ->subview(new SliceSelector('1:'));                          // [5, 7]
248
     *
249
     * $subview[':'] = [55, 77];
250
     * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10]
251
     * ```
252
     *
253
     * ##### Example (using short objects)
254
     * ```
255
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
256
     *
257
     * $subview = ArrayView::toView($source)
258
     *     ->subview('::2')                           // [1, 3, 5, 7, 9]
259
     *     ->subview([true, false, true, true, true]) // [1, 5, 7, 9]
260
     *     ->subview([0, 1, 2])                       // [1, 5, 7]
261
     *     ->subview('1:');                           // [5, 7]
262
     *
263
     * $subview[':'] = [55, 77];
264
     * print_r($source); // [1, 2, 3, 4, 55, 6, 77, 8, 9, 10]
265
     * ```
266
     *
267
     * ##### Readonly example
268
     * ```
269
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
270
     * $subview = ArrayView::toView($source)->subview('::2');
271
     *
272
     * $subview[':']; // [1, 3, 5, 7, 9]
273
     * $subview[':'] = [11, 33, 55, 77, 99]; // throws ReadonlyError
274
     * $subview[0] = [11]; // throws ReadonlyError
275
     * ```
276
     *
277
     * @template S of string|array<mixed>|ArrayViewInterface<mixed>|ArraySelectorInterface
278
     *
279
     * @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...
280
     * @param bool|null $readonly Flag indicating if the subview should be read-only.
281
     *
282
     * @return ArrayViewInterface<T> A new view representing the subview of this view.
283
     *
284
     * @throws IndexError if the selector is IndexListSelector and some indexes are out of range.
285
     * @throws SizeError if the selector is MaskSelector and size of the mask not equals to size of the view.
286
     * @throws KeyError if the selector is not valid (e.g. non-sequential array).
287
     */
288
    public function subview($selector, bool $readonly = null): ArrayViewInterface
289
    {
290
        return $this->toSelector($selector)->select($this, $readonly);
291
    }
292
293
    /**
294
     * Applies a transformation function to each element in the view.
295
     *
296
     * ##### Example
297
     * ```php
298
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
299
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9]
300
     *
301
     * $subview->apply(fn ($x) => $x * 10);
302
     *
303
     * $subview->toArray(); // [10, 30, 50, 70, 90]
304
     * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10]
305
     * ```
306
     *
307
     * @param callable(T, int): T $mapper Function to transform each element.
308
     *
309
     * @return ArrayView<T> this view.
310
     */
311
    public function apply(callable $mapper): self
312
    {
313
        $size = \count($this);
314
        for ($i = 0; $i < $size; $i++) {
315
            /** @var T $item */
316
            $item = $this[$i];
317
            $this[$i] = $mapper($item, $i);
318
        }
319
        return $this;
320
    }
321
322
    /**
323
     * Sets new values for the elements in the view.
324
     *
325
     * ##### Example
326
     * ```php
327
     * $source = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
328
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5, 7, 9]
329
     *
330
     * $data = [9, 27, 45, 63, 81];
331
     *
332
     * $subview->applyWith($data, fn ($lhs, $rhs) => $lhs * $rhs);
333
     * $subview->toArray(); // [10, 30, 50, 70, 90]
334
     *
335
     * $source; // [10, 2, 30, 4, 50, 6, 70, 8, 90, 10]
336
     * ```
337
     *
338
     * @template U Type of $data items.
339
     *
340
     * @param array<U>|ArrayViewInterface<U> $data
341
     * @param callable(T, U, int): T $mapper
342
     *
343
     * @return ArrayView<T> this view.
344
     *
345
     * @throws ValueError if the $data is not sequential array.
346
     * @throws SizeError if size of $data not equals to size of the view.
347
     */
348
    public function applyWith($data, callable $mapper): self
349
    {
350
        $this->checkSequential($data);
351
352
        [$dataSize, $thisSize] = [\count($data), \count($this)];
353
        if ($dataSize !== $thisSize) {
354
            throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
355
        }
356
357
        $dataView = ArrayView::toView($data);
358
359
        $size = \count($this);
360
        for ($i = 0; $i < $size; $i++) {
361
            /** @var T $lhs */
362
            $lhs = $this[$i];
363
            /** @var U $rhs */
364
            $rhs = $dataView[$i];
365
            $this[$i] = $mapper($lhs, $rhs, $i);
366
        }
367
368
        return $this;
369
    }
370
371
    /**
372
     * Sets new values for the elements in the view.
373
     *
374
     * ##### Example
375
     * ```php
376
     * $source = [1, 2, 3, 4, 5];
377
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
378
     *
379
     * $subview->set([11, 33, 55]);
380
     * $subview->toArray(); // [11, 33, 55]
381
     *
382
     * $source; // [11, 2, 33, 4, 55]
383
     * ```
384
     *
385
     * @param array<T>|ArrayViewInterface<T>|T $newValues The new values to set.
386
     *
387
     * @return ArrayView<T> this view.
388
     *
389
     * @throws ValueError if the $newValues is not sequential array.
390
     * @throws SizeError if size of $newValues not equals to size of the view.
391
     */
392
    public function set($newValues): self
393
    {
394
        $this->checkSequential($newValues);
395
396
        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...
introduced by
The condition is_array($newValues) is always false.
Loading history...
397
            $size = \count($this);
398
            for ($i = 0; $i < $size; $i++) {
399
                $this[$i] = $newValues;
400
            }
401
            return $this;
402
        }
403
404
        [$dataSize, $thisSize] = [\count($newValues), \count($this)];
405
        if ($dataSize !== $thisSize) {
406
            throw new SizeError("Length of values array not equal to view length ({$dataSize} != {$thisSize}).");
407
        }
408
409
        $newValuesView = ArrayView::toView($newValues);
410
411
        $size = \count($this);
412
        for ($i = 0; $i < $size; $i++) {
413
            $this[$i] = $newValuesView[$i];
414
        }
415
416
        return $this;
417
    }
418
419
    /**
420
     * Return iterator to iterate the view elements.
421
     *
422
     * ##### Example
423
     * ```php
424
     * $source = [1, 2, 3, 4, 5];
425
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
426
     *
427
     * foreach ($subview as $item) {
428
     *     // 1, 3, 5
429
     * }
430
     *
431
     * print_r([...$subview]); // [1, 3, 5]
432
     * ```
433
     *
434
     * @return \Generator<int, T>
435
     */
436
    public function getIterator(): \Generator
437
    {
438
        $size = \count($this);
439
        for ($i = 0; $i < $size; $i++) {
440
            /** @var T $item */
441
            $item = $this[$i];
442
            yield $item;
443
        }
444
    }
445
446
    /**
447
     * Return true if view is readonly, otherwise false.
448
     *
449
     * ##### Example
450
     * ```php
451
     * $source = [1, 2, 3, 4, 5];
452
     *
453
     * $readonlyView = ArrayView::toView($source, true);
454
     * $readonlyView->isReadonly(); // true
455
     *
456
     * $readonlySubview = ArrayView::toView($source)->subview('::2', true);
457
     * $readonlySubview->isReadonly(); // true
458
     *
459
     * $view = ArrayView::toView($source);
460
     * $view->isReadonly(); // false
461
     *
462
     * $subview = ArrayView::toView($source)->subview('::2');
463
     * $subview->isReadonly(); // false
464
     * ```
465
     *
466
     * @return bool
467
     */
468
    public function isReadonly(): bool
469
    {
470
        return $this->readonly;
471
    }
472
473
    /**
474
     * Return size of the view.
475
     *
476
     * ##### Example
477
     * ```php
478
     * $source = [1, 2, 3, 4, 5];
479
     *
480
     * $subview = ArrayView::toView($source)->subview('::2'); // [1, 3, 5]
481
     * count($subview); // 3
482
     * ```
483
     *
484
     * @return int
485
     */
486
    public function count(): int
487
    {
488
        return $this->getParentSize();
489
    }
490
491
    /**
492
     * Get the size of the parent view or source array.
493
     *
494
     * @return int The size of the parent view or source array.
495
     */
496
    protected function getParentSize(): int
497
    {
498
        return ($this->parentView !== null)
499
            ? \count($this->parentView)
500
            : \count($this->source);
501
    }
502
503
    /**
504
     * Check if the given source array is sequential (indexed from 0 to n-1).
505
     *
506
     * If the array is not sequential, a ValueError is thrown indicating that
507
     * a view cannot be created for a non-sequential array.
508
     *
509
     * @param mixed $source The source array to check for sequential indexing.
510
     *
511
     * @return void
512
     *
513
     * @throws ValueError if the source array is not sequential.
514
     */
515
    protected function checkSequential($source): void
516
    {
517
        if (is_array($source) && !Util::isArraySequential($source)) {
518
            throw new ValueError('Cannot create view for non-sequential array.');
519
        }
520
    }
521
522
    /**
523
     * Convert the given index to a valid index within the source array.
524
     *
525
     * @param int $i The index to convert.
526
     *
527
     * @return int The converted index within the source array.
528
     *
529
     * @throws IndexError if the index is out of range and $throwError is true.
530
     */
531
    protected function convertIndex(int $i): int
532
    {
533
        return Util::normalizeIndex($i, \count($this->source));
534
    }
535
536
    /**
537
     * Check if a numeric offset exists in the source array.
538
     *
539
     * @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...
540
     *
541
     * @return bool Returns true if the numeric offset exists in the source, false otherwise.
542
     */
543
    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...
544
    {
545
        // Non-string must be integer
546
        if (!\is_string($offset) && !\is_int($offset)) {
0 ignored issues
show
introduced by
The condition is_int($offset) is always false.
Loading history...
introduced by
The condition is_string($offset) is always false.
Loading history...
547
            return false;
548
        }
549
550
        // Numeric string must be integer
551
        if (!\is_integer($offset + 0)) {
552
            return false;
553
        }
554
555
        try {
556
            $index = $this->convertIndex(intval($offset));
557
        } catch (IndexError $e) {
558
            return false;
559
        }
560
561
        return \is_array($this->source)
562
            ? \array_key_exists($index, $this->source)
563
            : $this->source->offsetExists($index);
564
    }
565
}
566