NestedAccessor   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 430
Duplicated Lines 0 %

Importance

Changes 7
Bugs 3 Features 1
Metric Value
eloc 153
c 7
b 3
f 1
dl 0
loc 430
rs 3.6
wmc 60

20 Methods

Rating   Name   Duplication   Size   Complexity  
A isset() 0 13 3
A __construct() 0 4 1
A exist() 0 9 2
A get() 0 4 1
A checkExist() 0 3 1
A cutPathTail() 0 4 1
A append() 0 9 1
A getPathStack() 0 3 1
A checkIsArrayAccessible() 0 10 3
A delete() 0 21 4
A update() 0 5 1
A handleError() 0 7 3
D getInternal() 0 98 21
A set() 0 16 3
A getPathList() 0 20 5
A getRef() 0 21 5
A offsetExists() 0 3 1
A offsetGet() 0 4 1
A offsetSet() 0 3 1
A offsetUnset() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like NestedAccessor 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 NestedAccessor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Smoren\Schemator\Components;
6
7
use Smoren\Schemator\Exceptions\PathNotArrayAccessibleException;
8
use Smoren\Schemator\Exceptions\PathNotExistException;
9
use Smoren\Schemator\Exceptions\PathNotWritableException;
10
use Smoren\Schemator\Helpers\ContainerAccessHelper;
11
use Smoren\Schemator\Interfaces\NestedAccessorInterface;
12
use Smoren\Schemator\Interfaces\ProxyInterface;
13
14
/**
15
 * @implements NestedAccessorInterface<string|string[]|null>
16
 */
17
class NestedAccessor implements NestedAccessorInterface
18
{
19
    public const OPERATOR_FOR_EACH = '*';
20
    public const OPERATOR_PIPE = '|';
21
22
    /**
23
     * @var array<mixed>|object
24
     */
25
    protected $source;
26
    /**
27
     * @var non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
28
     */
29
    protected string $pathDelimiter;
30
31
    /**
32
     * NestedAccessor constructor.
33
     *
34
     * @param array<mixed>|object $source
35
     * @param non-empty-string $pathDelimiter
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
36
     */
37
    public function __construct(&$source, string $pathDelimiter = '.')
38
    {
39
        $this->source = &$source;
40
        $this->pathDelimiter = $pathDelimiter;
41
    }
42
43
    /**
44
     * {@inheritDoc}
45
     */
46
    public function exist($path, bool $strict = true): bool
47
    {
48
        [, $allExist, $someExist] = $this->getInternal($path, false);
49
50
        if ($strict) {
51
            return $allExist;
52
        }
53
54
        return $someExist;
55
    }
56
57
    /**
58
     * {@inheritDoc}
59
     */
60
    public function isset($path, bool $strict = true): bool
61
    {
62
        [$result, $allExist, $someExist] = $this->getInternal($path, false);
63
64
        if ($result === null) {
65
            return false;
66
        }
67
68
        if ($strict) {
69
            return $allExist;
70
        }
71
72
        return $someExist;
73
    }
74
75
    /**
76
     * {@inheritDoc}
77
     */
78
    public function get($path = null, bool $strict = true)
79
    {
80
        [$result] = $this->getInternal($path, $strict);
81
        return $result;
82
    }
83
84
    /**
85
     * {@inheritDoc}
86
     */
87
    public function set($path, $value): self
88
    {
89
        $source = &$this->getRef($this->getPathStack($path));
90
91
        if ($source instanceof ProxyInterface) {
92
            try {
93
                $source->setValue($value);
94
            } catch (\BadMethodCallException $e) {
95
                [$key, $path] = $this->cutPathTail($path);
96
                throw new PathNotWritableException($key, $path, $this->pathDelimiter);
97
            }
98
        } else {
99
            $source = $value;
100
        }
101
102
        return $this;
103
    }
104
105
    /**
106
     * {@inheritDoc}
107
     */
108
    public function update($path, $value): self
109
    {
110
        $this->checkExist($path);
111
112
        return $this->set($path, $value);
113
    }
114
115
    /**
116
     * {@inheritDoc}
117
     */
118
    public function append($path, $value): self
119
    {
120
        $this->checkIsArrayAccessible($path);
121
122
        /** @var array<mixed> $source */
123
        $source = &$this->getRef($this->getPathStack($path));
124
        $source[] = $value;
125
126
        return $this;
127
    }
128
129
    /**
130
     * {@inheritDoc}
131
     */
132
    public function delete($path, bool $strict = true): self
133
    {
134
        try {
135
            $this->checkExist($path);
136
        } catch (PathNotExistException $e) {
137
            if ($strict) {
138
                throw $e;
139
            }
140
            return $this;
141
        }
142
143
        [$key, $path] = $this->cutPathTail($path);
144
        $source = &$this->getRef($this->getPathStack($path));
145
146
        try {
147
            ContainerAccessHelper::delete($source, $key);
0 ignored issues
show
Bug introduced by
$key of type string is incompatible with the type Smoren\Schemator\Helpers\TKey expected by parameter $key of Smoren\Schemator\Helpers...rAccessHelper::delete(). ( Ignorable by Annotation )

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

147
            ContainerAccessHelper::delete($source, /** @scrutinizer ignore-type */ $key);
Loading history...
Bug introduced by
It seems like $source can also be of type Smoren\Schemator\Interfaces\ProxyInterface; however, parameter $container of Smoren\Schemator\Helpers...rAccessHelper::delete() does only seem to accept ArrayAccess|Smoren\Schemator\Helpers\TValue[], maybe add an additional type check? ( Ignorable by Annotation )

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

147
            ContainerAccessHelper::delete(/** @scrutinizer ignore-type */ $source, $key);
Loading history...
148
        } catch (\InvalidArgumentException $e) {
149
            throw new PathNotWritableException($key, $path, $this->pathDelimiter);
150
        }
151
152
        return $this;
153
    }
154
155
    /**
156
     * Checks if given path exist in container.
157
     *
158
     * @param string|string[]|null $path
159
     *
160
     * @throws PathNotExistException when path does not exist in container.
161
     * @throws \InvalidArgumentException when invalid path passed.
162
     */
163
    protected function checkExist($path): void
164
    {
165
        $this->get($path);
166
    }
167
168
    /**
169
     * Check if value by given path is array or ArrayAccess instance.
170
     *
171
     * @param string|string[]|null $path
172
     *
173
     * @return void
174
     *
175
     * @throws PathNotExistException when path does not exist in container.
176
     * @throws PathNotArrayAccessibleException if path is not an array or ArrayAccess instance.
177
     * @throws \InvalidArgumentException when invalid path passed.
178
     */
179
    protected function checkIsArrayAccessible($path): void
180
    {
181
        if (!$this->exist($path)) {
182
            [$key, $path] = $this->cutPathTail($path);
183
            throw new PathNotExistException($key, $path, $this->pathDelimiter);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null and string; however, parameter $path of Smoren\Schemator\Excepti...xception::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

183
            throw new PathNotExistException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
184
        }
185
186
        if (!ContainerAccessHelper::isArrayAccessible($this->get($path))) {
187
            [$key, $path] = $this->cutPathTail($path);
188
            throw new PathNotArrayAccessibleException($key, $path, $this->pathDelimiter);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null and string; however, parameter $path of Smoren\Schemator\Excepti...xception::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

188
            throw new PathNotArrayAccessibleException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
189
        }
190
    }
191
192
    /**
193
     * Cuts last key from given path.
194
     *
195
     * Returns array of last key and truncated path.
196
     *
197
     * @param string|string[]|null $path
198
     *
199
     * @return array{string, string[]} [lastKey, truncatedPath]
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, string[]} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
200
     *
201
     * @throws \InvalidArgumentException when invalid path passed.
202
     */
203
    protected function cutPathTail($path): array
204
    {
205
        $path = $this->getPathList($path);
206
        return [strval(array_pop($path)), $path];
207
    }
208
209
    /**
210
     * Returns ref to value stored by given path.
211
     *
212
     * Creates path if it does not exist.
213
     *
214
     * @param string[] $pathStack
215
     *
216
     * @return mixed|ProxyInterface<object>
217
     *
218
     * @throws PathNotWritableException when path is not writable.
219
     */
220
    protected function &getRef(array $pathStack)
221
    {
222
        $source = &$this->source;
223
        $traveledPath = [];
224
225
        while (count($pathStack)) {
226
            $pathItem = array_pop($pathStack);
227
            $traveledPath[] = $pathItem;
228
229
            try {
230
                $source = &ContainerAccessHelper::getRef($source, $pathItem, []);
0 ignored issues
show
Bug introduced by
array() of type array is incompatible with the type Smoren\Schemator\Helpers\TValue|null expected by parameter $defaultValue of Smoren\Schemator\Helpers...rAccessHelper::getRef(). ( Ignorable by Annotation )

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

230
                $source = &ContainerAccessHelper::getRef($source, $pathItem, /** @scrutinizer ignore-type */ []);
Loading history...
231
            } catch (\InvalidArgumentException $e) {
232
                throw new PathNotWritableException($pathItem, $traveledPath, $this->pathDelimiter);
233
            }
234
235
            if (count($pathStack) && is_scalar($source)) {
236
                $source = [];
237
            }
238
        }
239
240
        return $source;
241
    }
242
243
    /**
244
     * Converts given path to stack array.
245
     *
246
     * @param string|string[]|null $path
247
     *
248
     * @return string[]
249
     *
250
     * @throws \InvalidArgumentException when invalid path passed.
251
     */
252
    protected function getPathStack($path): array
253
    {
254
        return array_reverse($this->getPathList($path));
255
    }
256
257
    /**
258
     * Converts given path to array.
259
     *
260
     * @param string|string[]|mixed|null $path
261
     *
262
     * @return string[]
263
     *
264
     * @throws \InvalidArgumentException when invalid path passed.
265
     */
266
    protected function getPathList($path): array
267
    {
268
        if ($path === null) {
269
            return [];
270
        }
271
272
        if (is_string($path)) {
273
            $path = explode($this->pathDelimiter, $path);
274
        }
275
276
        if (is_numeric($path)) {
277
            $path = [strval($path)];
278
        }
279
280
        if (!is_array($path)) {
281
            $type = gettype($path);
282
            throw new \InvalidArgumentException("Path must be numeric, string or array, {$type} given");
283
        }
284
285
        return $path;
286
    }
287
288
    /**
289
     * Handle path errors.
290
     *
291
     * @param string $key
292
     * @param string[] $path
293
     * @param bool $isResultMultiple
294
     * @param bool $strict
295
     *
296
     * @return null|array{}
297
     *
298
     * @throws PathNotExistException always in strict mode.
299
     */
300
    protected function handleError(string $key, array $path, bool $isResultMultiple, bool $strict): ?array
301
    {
302
        if (!$strict) {
303
            return $isResultMultiple ? [] : null;
304
        }
305
306
        throw new PathNotExistException($key, $path, $this->pathDelimiter);
307
    }
308
309
    /**
310
     * @param string|string[]|null $path
311
     * @param bool $strict
312
     * @return array{mixed, bool, bool, bool}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{mixed, bool, bool, bool} at position 2 could not be parsed: Expected ':' at position 2, but found 'mixed'.
Loading history...
313
     */
314
    public function getInternal($path = null, bool $strict = true): array
315
    {
316
        $carry = $this->source;
317
        $pathStack = $this->getPathStack($path);
318
        $traveledPath = [];
319
        $isResultMultiple = false;
320
        $allExist = true;
321
322
        while (count($pathStack)) {
323
            $key = array_pop($pathStack);
324
            if ($key === static::OPERATOR_PIPE) {
325
                $isResultMultiple = false;
326
                $traveledPath[] = $key;
327
                continue;
328
            }
329
330
            $opForEach = static::OPERATOR_FOR_EACH;
331
            if (preg_match("/^[{$opForEach}]+$/", strval($key))) {
332
                for ($i = 0; $i < strlen($key) - 1; ++$i) {
333
                    $pathStack[] = static::OPERATOR_FOR_EACH;
334
                }
335
                $key = static::OPERATOR_FOR_EACH;
336
            }
337
338
            if ($key === static::OPERATOR_FOR_EACH) {
339
                if (!is_iterable($carry)) {
340
                    return [
341
                        $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError(strva...esultMultiple, $strict) targeting Smoren\Schemator\Compone...Accessor::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
342
                        false,
343
                        false,
344
                        $isResultMultiple,
345
                    ];
346
                }
347
348
                $result = [];
349
350
                if ($isResultMultiple) {
351
                    foreach ($carry as $item) {
352
                        if (!is_iterable($item)) {
353
                            if ($strict) {
354
                                $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict);
355
                            }
356
                            $allExist = false;
357
                            continue;
358
                        }
359
                        foreach ($item as $subItem) {
360
                            $result[] = $subItem;
361
                        }
362
                    }
363
                } else {
364
                    foreach ($carry as $item) {
365
                        $result[] = $item;
366
                    }
367
                }
368
369
                $isResultMultiple = true;
370
                $traveledPath[] = $key;
371
                $carry = $result;
372
373
                continue;
374
            }
375
376
            if ($isResultMultiple) {
377
                $result = [];
378
                /** @var iterable<mixed> $carry */
379
                foreach ($carry as $item) {
380
                    if (!ContainerAccessHelper::exist($item, $key)) {
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type string; however, parameter $key of Smoren\Schemator\Helpers...erAccessHelper::exist() does only seem to accept Smoren\Schemator\Helpers\TKey, maybe add an additional type check? ( Ignorable by Annotation )

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

380
                    if (!ContainerAccessHelper::exist($item, /** @scrutinizer ignore-type */ $key)) {
Loading history...
381
                        if ($strict) {
382
                            $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict);
383
                        }
384
                        $allExist = false;
385
                        continue;
386
                    }
387
                    $result[] = ContainerAccessHelper::get($item, $key);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type string; however, parameter $key of Smoren\Schemator\Helpers...inerAccessHelper::get() does only seem to accept Smoren\Schemator\Helpers\TKey, maybe add an additional type check? ( Ignorable by Annotation )

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

387
                    $result[] = ContainerAccessHelper::get($item, /** @scrutinizer ignore-type */ $key);
Loading history...
388
                }
389
                $traveledPath[] = $key;
390
                $carry = $result;
391
392
                continue;
393
            }
394
395
            if (!ContainerAccessHelper::exist($carry, $key)) {
396
                return [
397
                    $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError(strva...esultMultiple, $strict) targeting Smoren\Schemator\Compone...Accessor::handleError() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
398
                    false,
399
                    false,
400
                    $isResultMultiple,
401
                ];
402
            }
403
404
            $carry = ContainerAccessHelper::get($carry, $key);
405
            $traveledPath[] = $key;
406
        }
407
408
        $someExist = !$isResultMultiple || (is_array($carry) && count($carry));
409
        $allExist = $allExist && $someExist;
410
411
        return [$carry, $allExist, $someExist, $isResultMultiple];
412
    }
413
414
    /**
415
     * {@inheritDoc}
416
     */
417
    public function offsetExists($offset): bool
418
    {
419
        return $this->exist($offset);
420
    }
421
422
    /**
423
     * {@inheritDoc}
424
     *
425
     * @return mixed
426
     */
427
    #[\ReturnTypeWillChange]
428
    public function offsetGet($offset)
429
    {
430
        return $this->get($offset);
431
    }
432
433
    /**
434
     * {@inheritDoc}
435
     */
436
    public function offsetSet($offset, $value): void
437
    {
438
        $this->set($offset, $value);
439
    }
440
441
    /**
442
     * {@inheritDoc}
443
     */
444
    public function offsetUnset($offset): void
445
    {
446
        $this->delete($offset);
447
    }
448
}
449