Passed
Push — master ( 6434d9...1aee48 )
by Smoren
02:19
created

NestedAccessor   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 144
dl 0
loc 388
rs 6.96
c 4
b 0
f 1
wmc 53

16 Methods

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

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\PathNotArrayException;
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
13
/**
14
 * @implements NestedAccessorInterface<string|string[]|null>
15
 */
16
class NestedAccessor implements NestedAccessorInterface
17
{
18
    public const OPERATOR_FOR_EACH = '*';
19
    public const OPERATOR_PIPE = '|';
20
21
    /**
22
     * @var array<mixed>|object
23
     */
24
    protected $source;
25
    /**
26
     * @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...
27
     */
28
    protected string $pathDelimiter;
29
30
    /**
31
     * NestedAccessor constructor.
32
     *
33
     * @param array<mixed>|object $source
34
     * @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...
35
     */
36
    public function __construct(&$source, string $pathDelimiter = '.')
37
    {
38
        $this->source = &$source;
39
        $this->pathDelimiter = $pathDelimiter;
40
    }
41
42
    /**
43
     * {@inheritDoc}
44
     */
45
    public function exist($path, bool $strict = true): bool
46
    {
47
        [, $allExist, $someExist] = $this->getInternal($path, false);
48
49
        if ($strict) {
50
            return $allExist;
51
        }
52
53
        return $someExist;
54
    }
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    public function isset($path, bool $strict = true): bool
60
    {
61
        [$result, $allExist, $someExist] = $this->getInternal($path, false);
62
63
        if ($result === null) {
64
            return false;
65
        }
66
67
        if ($strict) {
68
            return $allExist;
69
        }
70
71
        return $someExist;
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77
    public function get($path = null, bool $strict = true)
78
    {
79
        [$result] = $this->getInternal($path, $strict);
80
        return $result;
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     */
86
    public function set($path, $value): self
87
    {
88
        $source = &$this->getRef($this->getPathStack($path));
89
        $source = $value;
90
91
        return $this;
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     */
97
    public function update($path, $value): self
98
    {
99
        $this->checkExist($path);
100
101
        return $this->set($path, $value);
102
    }
103
104
    /**
105
     * {@inheritDoc}
106
     */
107
    public function append($path, $value): self
108
    {
109
        $this->checkIsArrayAccessible($path);
110
111
        /** @var array<mixed> $source */
112
        $source = &$this->getRef($this->getPathStack($path));
113
        $source[] = $value;
114
115
        return $this;
116
    }
117
118
    /**
119
     * {@inheritDoc}
120
     */
121
    public function delete($path, bool $strict = true): self
122
    {
123
        try {
124
            $this->checkExist($path);
125
        } catch (PathNotExistException $e) {
126
            if ($strict) {
127
                throw $e;
128
            }
129
            return $this;
130
        }
131
132
        [$key, $path] = $this->cutPathTail($path);
133
        $source = &$this->getRef($this->getPathStack($path));
134
135
        try {
136
            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

136
            ContainerAccessHelper::delete($source, /** @scrutinizer ignore-type */ $key);
Loading history...
137
        } catch (\InvalidArgumentException $e) {
138
            throw new PathNotWritableException($key, $path, $this->pathDelimiter);
139
        }
140
141
        return $this;
142
    }
143
144
    /**
145
     * Checks if given path exist in container.
146
     *
147
     * @param string|string[]|null $path
148
     *
149
     * @throws PathNotExistException when path does not exist in container.
150
     * @throws \InvalidArgumentException when invalid path passed.
151
     */
152
    protected function checkExist($path): void
153
    {
154
        $this->get($path);
155
    }
156
157
    /**
158
     * Check if value by given path is array or ArrayAccess instance.
159
     *
160
     * @param string|string[]|null $path
161
     *
162
     * @return void
163
     *
164
     * @throws PathNotExistException when path does not exist in container.
165
     * @throws PathNotArrayException if path is not an array or ArrayAccess instance.
166
     * @throws \InvalidArgumentException when invalid path passed.
167
     */
168
    protected function checkIsArrayAccessible($path): void
169
    {
170
        if (!$this->exist($path)) {
171
            [$key, $path] = $this->cutPathTail($path);
172
            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

172
            throw new PathNotExistException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
173
        }
174
175
        if (!ContainerAccessHelper::isArrayAccessible($this->get($path))) {
176
            [$key, $path] = $this->cutPathTail($path);
177
            throw new PathNotArrayException($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

177
            throw new PathNotArrayException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
178
        }
179
    }
180
181
    /**
182
     * Cuts last key from given path.
183
     *
184
     * Returns array of last key and truncated path.
185
     *
186
     * @param string|string[]|null $path
187
     *
188
     * @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...
189
     *
190
     * @throws \InvalidArgumentException when invalid path passed.
191
     */
192
    protected function cutPathTail($path): array
193
    {
194
        $path = $this->getPathList($path);
195
        return [strval(array_pop($path)), $path];
196
    }
197
198
    /**
199
     * Returns ref to value stored by given path.
200
     *
201
     * Creates path if it does not exist.
202
     *
203
     * @param string[] $pathStack
204
     *
205
     * @return mixed
206
     *
207
     * @throws PathNotWritableException when path is not writable.
208
     */
209
    protected function &getRef(array $pathStack)
210
    {
211
        $source = &$this->source;
212
        $traveledPath = [];
213
214
        while (count($pathStack)) {
215
            $pathItem = array_pop($pathStack);
216
            $traveledPath[] = $pathItem;
217
218
            try {
219
                $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

219
                $source = &ContainerAccessHelper::getRef($source, $pathItem, /** @scrutinizer ignore-type */ []);
Loading history...
220
            } catch (\InvalidArgumentException $e) {
221
                throw new PathNotWritableException($pathItem, $traveledPath, $this->pathDelimiter);
222
            }
223
224
            if (count($pathStack) && is_scalar($source)) {
225
                $source = [];
226
            }
227
        }
228
229
        return $source;
230
    }
231
232
    /**
233
     * Converts given path to stack array.
234
     *
235
     * @param string|string[]|null $path
236
     *
237
     * @return string[]
238
     *
239
     * @throws \InvalidArgumentException when invalid path passed.
240
     */
241
    protected function getPathStack($path): array
242
    {
243
        return array_reverse($this->getPathList($path));
244
    }
245
246
    /**
247
     * Converts given path to array.
248
     *
249
     * @param string|string[]|mixed|null $path
250
     *
251
     * @return string[]
252
     *
253
     * @throws \InvalidArgumentException when invalid path passed.
254
     */
255
    protected function getPathList($path): array
256
    {
257
        if ($path === null) {
258
            return [];
259
        }
260
261
        if (is_string($path)) {
262
            $path = explode($this->pathDelimiter, $path);
263
        }
264
265
        if (is_numeric($path)) {
266
            $path = [strval($path)];
267
        }
268
269
        if (!is_array($path)) {
270
            $type = gettype($path);
271
            throw new \InvalidArgumentException("Path must be numeric, string or array, {$type} given");
272
        }
273
274
        return $path;
275
    }
276
277
    /**
278
     * Handle path errors.
279
     *
280
     * @param string $key
281
     * @param string[] $path
282
     * @param bool $isResultMultiple
283
     * @param bool $strict
284
     *
285
     * @return null|array{}
286
     *
287
     * @throws PathNotExistException always in strict mode.
288
     */
289
    protected function handleError(string $key, array $path, bool $isResultMultiple, bool $strict): ?array
290
    {
291
        if (!$strict) {
292
            return $isResultMultiple ? [] : null;
293
        }
294
295
        throw new PathNotExistException($key, $path, $this->pathDelimiter);
296
    }
297
298
    /**
299
     * @param string|string[]|null $path
300
     * @param bool $strict
301
     * @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...
302
     */
303
    public function getInternal($path = null, bool $strict = true): array
304
    {
305
        $carry = $this->source;
306
        $pathStack = $this->getPathStack($path);
307
        $traveledPath = [];
308
        $isResultMultiple = false;
309
        $allExist = true;
310
311
        while (count($pathStack)) {
312
            $key = array_pop($pathStack);
313
            if ($key === static::OPERATOR_PIPE) {
314
                $isResultMultiple = false;
315
                $traveledPath[] = $key;
316
                continue;
317
            }
318
319
            $opForEach = static::OPERATOR_FOR_EACH;
320
            if (preg_match("/^[{$opForEach}]+$/", strval($key))) {
321
                for ($i = 0; $i < strlen($key) - 1; ++$i) {
322
                    $pathStack[] = static::OPERATOR_FOR_EACH;
323
                }
324
                $key = static::OPERATOR_FOR_EACH;
325
            }
326
327
            if ($key === static::OPERATOR_FOR_EACH) {
328
                if (!is_iterable($carry)) {
329
                    return [
330
                        $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...
331
                        false,
332
                        false,
333
                        $isResultMultiple,
334
                    ];
335
                }
336
337
                $result = [];
338
339
                if ($isResultMultiple) {
340
                    foreach ($carry as $item) {
341
                        if (!is_iterable($item)) {
342
                            if ($strict) {
343
                                $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict);
344
                            }
345
                            $allExist = false;
346
                            continue;
347
                        }
348
                        foreach ($item as $subItem) {
349
                            $result[] = $subItem;
350
                        }
351
                    }
352
                } else {
353
                    foreach ($carry as $item) {
354
                        $result[] = $item;
355
                    }
356
                }
357
358
                $isResultMultiple = true;
359
                $traveledPath[] = $key;
360
                $carry = $result;
361
362
                continue;
363
            }
364
365
            if ($isResultMultiple) {
366
                $result = [];
367
                /** @var iterable<mixed> $carry */
368
                foreach ($carry as $item) {
369
                    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

369
                    if (!ContainerAccessHelper::exist($item, /** @scrutinizer ignore-type */ $key)) {
Loading history...
370
                        if ($strict) {
371
                            $this->handleError(strval($key), $traveledPath, $isResultMultiple, $strict);
372
                        }
373
                        $allExist = false;
374
                        continue;
375
                    }
376
                    $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

376
                    $result[] = ContainerAccessHelper::get($item, /** @scrutinizer ignore-type */ $key);
Loading history...
377
                }
378
                $traveledPath[] = $key;
379
                $carry = $result;
380
381
                continue;
382
            }
383
384
            if (!ContainerAccessHelper::exist($carry, $key)) {
385
                return [
386
                    $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...
387
                    false,
388
                    false,
389
                    $isResultMultiple,
390
                ];
391
            }
392
393
            $carry = ContainerAccessHelper::get($carry, $key);
394
            $traveledPath[] = $key;
395
        }
396
397
        $someExist = !$isResultMultiple || count($carry);
0 ignored issues
show
Bug introduced by
It seems like $carry can also be of type null; however, parameter $value of count() does only seem to accept Countable|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

397
        $someExist = !$isResultMultiple || count(/** @scrutinizer ignore-type */ $carry);
Loading history...
398
399
        return [
400
            $carry,
401
            $allExist && $someExist,
402
            $someExist,
403
            $isResultMultiple
404
        ];
405
    }
406
}
407