Passed
Push — master ( 5f88dd...10f6bc )
by Smoren
12:50
created

NestedAccessor::exist()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Smoren\Schemator\Components;
4
5
use Smoren\Schemator\Exceptions\PathNotArrayException;
6
use Smoren\Schemator\Exceptions\PathNotExistException;
7
use Smoren\Schemator\Exceptions\PathNotWritableException;
8
use Smoren\Schemator\Helpers\ContainerAccessHelper;
9
use Smoren\Schemator\Interfaces\NestedAccessorInterface;
10
11
/**
12
 * @implements NestedAccessorInterface<string|string[]|null>
13
 */
14
class NestedAccessor implements NestedAccessorInterface
15
{
16
    public const OPERATOR_FOR_EACH = '*';
17
    public const OPERATOR_PIPE = '|';
18
19
    /**
20
     * @var array<mixed>|object
21
     */
22
    protected $source;
23
    /**
24
     * @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...
25
     */
26
    protected string $pathDelimiter;
27
28
    /**
29
     * NestedAccessor constructor.
30
     *
31
     * @param array<mixed>|object $source
32
     * @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...
33
     */
34
    public function __construct(&$source, string $pathDelimiter = '.')
35
    {
36
        $this->source = &$source;
37
        $this->pathDelimiter = $pathDelimiter;
38
    }
39
40
    /**
41
     * {@inheritDoc}
42
     */
43
    public function exist($path): bool
44
    {
45
        try {
46
            $this->get($path);
47
            return true;
48
        } catch (PathNotExistException $e) {
49
            return false;
50
        }
51
    }
52
53
    /**
54
     * {@inheritDoc}
55
     */
56
    public function isset($path): bool
57
    {
58
        try {
59
            return $this->get($path) !== null;
60
        } catch (PathNotExistException $e) {
61
            return false;
62
        }
63
    }
64
65
    /**
66
     * {@inheritDoc}
67
     */
68
    public function get($path = null, bool $strict = true)
69
    {
70
        $carry = $this->source;
71
        $pathStack = $this->getPathStack($path);
72
        $traveledPath = [];
73
        $isResultMultiple = false;
74
75
        while (count($pathStack)) {
76
            $key = array_pop($pathStack);
77
            if ($key === static::OPERATOR_PIPE) {
78
                $isResultMultiple = false;
79
                $traveledPath[] = $key;
80
                continue;
81
            }
82
83
            $opForEach = static::OPERATOR_FOR_EACH;
84
            if (preg_match("/^[{$opForEach}]+$/", $key)) {
85
                for ($i = 0; $i < strlen($key) - 1; ++$i) {
86
                    $pathStack[] = static::OPERATOR_FOR_EACH;
87
                }
88
                $key = static::OPERATOR_FOR_EACH;
89
            }
90
91
            if ($key === static::OPERATOR_FOR_EACH) {
92
                if (!is_iterable($carry)) {
93
                    return $this->handleError($key, $traveledPath, $isResultMultiple, $strict);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($key,...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...
94
                }
95
96
                $result = [];
97
98
                if ($isResultMultiple) {
99
                    foreach ($carry as $item) {
100
                        if (!is_iterable($item)) {
101
                            if ($strict) {
102
                                return $this->handleError($key, $traveledPath, $isResultMultiple, $strict);
103
                            }
104
                            continue;
105
                        }
106
                        foreach ($item as $subItem) {
107
                            $result[] = $subItem;
108
                        }
109
                    }
110
                } else {
111
                    foreach ($carry as $item) {
112
                        $result[] = $item;
113
                    }
114
                }
115
116
                $isResultMultiple = true;
117
                $traveledPath[] = $key;
118
                $carry = $result;
119
120
                continue;
121
            }
122
123
            if ($isResultMultiple) {
124
                $result = [];
125
                /** @var iterable<mixed> $carry */
126
                foreach ($carry as $item) {
127
                    if (!ContainerAccessHelper::exists($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...rAccessHelper::exists() 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

127
                    if (!ContainerAccessHelper::exists($item, /** @scrutinizer ignore-type */ $key)) {
Loading history...
128
                        if ($strict) {
129
                            return $this->handleError($key, $traveledPath, $isResultMultiple, $strict);
130
                        }
131
                        continue;
132
                    }
133
                    $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

133
                    $result[] = ContainerAccessHelper::get($item, /** @scrutinizer ignore-type */ $key);
Loading history...
134
                }
135
                $traveledPath[] = $key;
136
                $carry = $result;
137
138
                continue;
139
            }
140
141
            if (!ContainerAccessHelper::exists($carry, $key)) {
142
                return $this->handleError($key, $traveledPath, $isResultMultiple, $strict);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->handleError($key,...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...
143
            }
144
145
            $carry = ContainerAccessHelper::get($carry, $key);
146
            $traveledPath[] = $key;
147
        }
148
149
        return $carry;
150
    }
151
152
    /**
153
     * {@inheritDoc}
154
     */
155
    public function set($path, $value): self
156
    {
157
        $source = &$this->getRef($this->getPathStack($path));
158
        $source = $value;
159
160
        return $this;
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     */
166
    public function update($path, $value): self
167
    {
168
        $this->checkExist($path);
169
170
        return $this->set($path, $value);
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function append($path, $value): self
177
    {
178
        $this->checkIsArrayAccessible($path);
179
180
        /** @var array<mixed> $source */
181
        $source = &$this->getRef($this->getPathStack($path));
182
        $source[] = $value;
183
184
        return $this;
185
    }
186
187
    /**
188
     * {@inheritDoc}
189
     */
190
    public function delete($path, bool $strict = true): self
191
    {
192
        try {
193
            $this->checkExist($path);
194
        } catch (PathNotExistException $e) {
195
            if ($strict) {
196
                throw $e;
197
            }
198
            return $this;
199
        }
200
201
        [$key, $path] = $this->cutPathTail($path);
202
        $source = &$this->getRef($this->getPathStack($path));
203
204
        try {
205
            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

205
            ContainerAccessHelper::delete($source, /** @scrutinizer ignore-type */ $key);
Loading history...
206
        } catch (\InvalidArgumentException $e) {
207
            throw new PathNotWritableException($key, $path, $this->pathDelimiter);
208
        }
209
210
        return $this;
211
    }
212
213
    /**
214
     * Checks if given path exist in container.
215
     *
216
     * @param string|string[]|null $path
217
     *
218
     * @throws PathNotExistException when path does not exist in container.
219
     * @throws \InvalidArgumentException when invalid path passed.
220
     */
221
    protected function checkExist($path): void
222
    {
223
        $this->get($path);
224
    }
225
226
    /**
227
     * Check if value by given path is array or ArrayAccess instance.
228
     *
229
     * @param string|string[]|null $path
230
     *
231
     * @return void
232
     *
233
     * @throws PathNotExistException when path does not exist in container.
234
     * @throws PathNotArrayException if path is not an array or ArrayAccess instance.
235
     * @throws \InvalidArgumentException when invalid path passed.
236
     */
237
    protected function checkIsArrayAccessible($path): void
238
    {
239
        if (!$this->exist($path)) {
240
            [$key, $path] = $this->cutPathTail($path);
241
            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

241
            throw new PathNotExistException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
242
        }
243
244
        if (!ContainerAccessHelper::isArrayAccessible($this->get($path))) {
245
            [$key, $path] = $this->cutPathTail($path);
246
            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

246
            throw new PathNotArrayException($key, /** @scrutinizer ignore-type */ $path, $this->pathDelimiter);
Loading history...
247
        }
248
    }
249
250
    /**
251
     * Cuts last key from given path.
252
     *
253
     * Returns array of last key and truncated path.
254
     *
255
     * @param string|string[]|null $path
256
     *
257
     * @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...
258
     *
259
     * @throws \InvalidArgumentException when invalid path passed.
260
     */
261
    protected function cutPathTail($path): array
262
    {
263
        $path = $this->getPathList($path);
264
        return [strval(array_pop($path)), $path];
265
    }
266
267
    /**
268
     * Returns ref to value stored by given path.
269
     *
270
     * Creates path if it does not exist.
271
     *
272
     * @param string[] $pathStack
273
     *
274
     * @return mixed
275
     *
276
     * @throws PathNotWritableException when path is not writable.
277
     */
278
    protected function &getRef(array $pathStack)
279
    {
280
        $source = &$this->source;
281
        $traveledPath = [];
282
283
        while (count($pathStack)) {
284
            $pathItem = array_pop($pathStack);
285
            $traveledPath[] = $pathItem;
286
287
            try {
288
                $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

288
                $source = &ContainerAccessHelper::getRef($source, $pathItem, /** @scrutinizer ignore-type */ []);
Loading history...
289
            } catch (\InvalidArgumentException $e) {
290
                throw new PathNotWritableException($pathItem, $traveledPath, $this->pathDelimiter);
291
            }
292
293
            if (count($pathStack) && is_scalar($source)) {
294
                $source = [];
295
            }
296
        }
297
298
        return $source;
299
    }
300
301
    /**
302
     * Converts given path to stack array.
303
     *
304
     * @param string|string[]|null $path
305
     *
306
     * @return string[]
307
     *
308
     * @throws \InvalidArgumentException when invalid path passed.
309
     */
310
    protected function getPathStack($path): array
311
    {
312
        return array_reverse($this->getPathList($path));
313
    }
314
315
    /**
316
     * Converts given path to array.
317
     *
318
     * @param string|string[]|mixed|null $path
319
     *
320
     * @return string[]
321
     *
322
     * @throws \InvalidArgumentException when invalid path passed.
323
     */
324
    protected function getPathList($path): array
325
    {
326
        if ($path === null) {
327
            return [];
328
        }
329
330
        if (is_string($path)) {
331
            $path = explode($this->pathDelimiter, $path);
332
        }
333
334
        if (is_numeric($path)) {
335
            $path = [strval($path)];
336
        }
337
338
        if (!is_array($path)) {
339
            $type = gettype($path);
340
            throw new \InvalidArgumentException("Path must be numeric, string or array, {$type} given");
341
        }
342
343
        return $path;
344
    }
345
346
    /**
347
     * Handle path errors.
348
     *
349
     * @param string $key
350
     * @param string[] $path
351
     * @param bool $isResultMultiple
352
     * @param bool $strict
353
     *
354
     * @return null|array{}
355
     *
356
     * @throws PathNotExistException always in strict mode.
357
     */
358
    protected function handleError(string $key, array $path, bool $isResultMultiple, bool $strict): ?array
359
    {
360
        if (!$strict) {
361
            return $isResultMultiple ? [] : null;
362
        }
363
364
        throw new PathNotExistException($key, $path, $this->pathDelimiter);
365
    }
366
}
367