NestedAccessor   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 136
dl 0
loc 367
rs 3.28
c 4
b 0
f 0
wmc 64

14 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 4 1
A setSource() 0 13 3
A append() 0 4 1
A exist() 0 7 2
A __construct() 0 4 1
A delete() 0 15 3
A isset() 0 6 2
A get() 0 31 5
A isLastKeyInteger() 0 4 1
A formatPath() 0 11 4
C _set() 0 45 12
D _get() 0 71 18
A _delete() 0 19 6
A _append() 0 14 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
namespace Smoren\NestedAccessor\Components;
4
5
use Smoren\NestedAccessor\Helpers\ArrayHelper;
6
use Smoren\NestedAccessor\Interfaces\NestedAccessorInterface;
7
use Smoren\NestedAccessor\Exceptions\NestedAccessorException;
8
use Smoren\TypeTools\MapAccess;
9
use Smoren\TypeTools\ObjectAccess;
10
use stdClass;
11
12
/**
13
 * Accessor class for getting and setting to source array or object with nested keys
14
 * @author Smoren <[email protected]>
15
 */
16
class NestedAccessor implements NestedAccessorInterface
17
{
18
    public const SET_MODE_SET = 1;
19
    public const SET_MODE_APPEND = 2;
20
    public const SET_MODE_DELETE = 3;
21
22
    /**
23
     * @var array<int|string, mixed>|object data source for accessing
24
     */
25
    protected $source;
26
    /**
27
     * @var non-empty-string path's separator of nesting
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
     * ArrayNestedAccessor constructor.
33
     * @param array<scalar, 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
     * @throws NestedAccessorException
36
     */
37
    public function __construct(&$source, string $pathDelimiter = '.')
38
    {
39
        $this->setSource($source);
40
        $this->pathDelimiter = $pathDelimiter;
41
    }
42
43
    /**
44
     * Setter for source
45
     * @param array<scalar, mixed>|object $source source setter
46
     * @return void
47
     * @throws NestedAccessorException
48
     */
49
    public function setSource(&$source): void
50
    {
51
        /** @var array<scalar, mixed>|object|mixed|null $source */
52
        if($source === null) {
53
            $source = [];
54
        }
55
56
        if(is_scalar($source)) {
57
            throw NestedAccessorException::createAsSourceIsScalar($source);
58
        }
59
60
        /** @var array<int|string, mixed>|object $source */
61
        $this->source = &$source;
62
    }
63
64
    /**
65
     * {@inheritDoc}
66
     */
67
    public function get($path = null, bool $strict = true)
68
    {
69
        // when path is not specified
70
        if($path === null || $path === '') {
71
            // let's return the full source
72
            return $this->source;
73
        }
74
75
        $path = $this->formatPath($path);
76
77
        // let result be null and there are no errors by default
78
        $result = null;
79
        $errorsCount = 0;
80
81
        // getting result with internal recursive method
82
        $this->_get(
83
            $this->source,
84
            array_reverse($path), // path stack
85
            $result,
86
            $errorsCount
87
        );
88
89
        // when strict mode is on and we got errors
90
        if($strict && $errorsCount) {
91
            throw NestedAccessorException::createAsCannotGetValue(
92
                implode($this->pathDelimiter, $path),
93
                $errorsCount
94
            );
95
        }
96
97
        return $result;
98
    }
99
100
    /**
101
     * {@inheritDoc}
102
     */
103
    public function set($path, $value, bool $strict = true): self
104
    {
105
        $path = $this->formatPath($path);
106
        return $this->_set($this->source, $path, $value, self::SET_MODE_SET, $strict);
107
    }
108
109
    /**
110
     * {@inheritDoc}
111
     */
112
    public function append($path, $value, bool $strict = true): self
113
    {
114
        $path = $this->formatPath($path);
115
        return $this->_set($this->source, $path, $value, self::SET_MODE_APPEND, $strict);
116
    }
117
118
    /**
119
     * {@inheritDoc}
120
     */
121
    public function delete($path, bool $strict = true): self
122
    {
123
        $path = $this->formatPath($path);
124
125
        if(!$this->exist($path)) {
126
            if($strict) {
127
                throw NestedAccessorException::createAsCannotSetValue(
128
                    self::SET_MODE_DELETE,
129
                    implode($this->pathDelimiter, $path)
130
                );
131
            }
132
            return $this;
133
        }
134
135
        return $this->_set($this->source, $path, null, self::SET_MODE_DELETE, $strict);
136
    }
137
138
    /**
139
     * {@inheritDoc}
140
     */
141
    public function exist($path): bool
142
    {
143
        try {
144
            $this->get($path);
145
            return true;
146
        } catch(NestedAccessorException $e) {
147
            return false;
148
        }
149
    }
150
151
    /**
152
     * {@inheritDoc}
153
     */
154
    public function isset($path): bool
155
    {
156
        try {
157
            return $this->get($path) !== null;
158
        } catch(NestedAccessorException $e) {
159
            return false;
160
        }
161
    }
162
163
    /**
164
     * Internal recursive method to get value from source by path stack
165
     * @param mixed $source source to get value from
166
     * @param array<string> $path nested path stack
167
     * @param array<scalar, mixed>|mixed $result place for result
168
     * @param int $errorsCount errors counter
169
     * @return void
170
     */
171
    protected function _get($source, array $path, &$result, int &$errorsCount): void
172
    {
173
        // let's iterate every path part from stack
174
        while(count($path)) {
175
            if(is_array($source) && $this->isLastKeyInteger($path)) {
176
                $key = array_pop($path);
177
178
                if (!array_key_exists($key, $source)) {
179
                    $errorsCount++;
180
                    return;
181
                }
182
183
                $source = $source[$key];
184
185
                continue;
186
            }
187
188
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
189
                // the result will be multiple
190
                if(!is_array($result)) {
191
                    $result = [];
192
                }
193
                // and we need to use recursive call for each item of this array
194
                foreach($source as $item) {
195
                    $this->_get($item, $path, $result, $errorsCount);
196
                }
197
                // we don't need to do something in this recursive branch
198
                return;
199
            }
200
201
            $key = array_pop($path);
202
203
            if(MapAccess::exists($source, $key)) {
204
                // go to the next nested level
205
                $source = MapAccess::get($source, $key);
206
            } else {
207
                // path part key is missing in source object
208
                $errorsCount++;
209
                // we cannot go deeper
210
                return;
211
            }
212
213
            // when it's not the last iteration of the stack
214
            // and the source is non-associative array (list)
215
            /** @var mixed $source */
216
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
217
                if(is_array($source) && $this->isLastKeyInteger($path)) {
218
                    continue;
219
                }
220
221
                // the result will be multiple
222
                if(!is_array($result)) {
223
                    $result = [];
224
                }
225
                // and we need to use recursive call for each item of this array
226
                foreach($source as $item) {
227
                    $this->_get($item, $path, $result, $errorsCount);
228
                }
229
                // we don't need to do something in this recursive branch
230
                return;
231
            }
232
        }
233
234
        // now path stack is empty — we reached target value of given path in source argument
235
        // so if result is multiple
236
        if(is_array($result)) {
237
            // we append source to result
238
            $result[] = $source;
239
        } else {
240
            // result is single
241
            $result = $source;
242
        }
243
        // that's all folks!
244
    }
245
246
    /**
247
     * Internal recursive method to save value to source by path stack
248
     * @param array<scalar, mixed>|object $source source to save value to
249
     * @param array<string> $path nested path
250
     * @param mixed $value value to save to source
251
     * @param int $mode when true append or set
252
     * @param bool $strict when true throw exception if path not exist in source object
253
     * @return $this
254
     * @throws NestedAccessorException
255
     */
256
    protected function _set(&$source, array $path, $value, int $mode, bool $strict): self
257
    {
258
        $temp = &$source;
259
        $tempPrevSource = null;
260
        $tempPrevKey = null;
261
262
        // let's iterate every path part to go deeper into nesting
263
        foreach($path as $key) {
264
            if(isset($temp) && is_scalar($temp)) {
265
                // value in the middle of the path must be an array
266
                $temp = [];
267
            }
268
269
            $tempPrevSource = &$temp;
270
            $tempPrevKey = $key;
271
272
            // go to the next nested level
273
            if(is_object($temp)) {
274
                if($strict && !($temp instanceof stdClass) && !ObjectAccess::hasPublicProperty($temp, $key)) {
275
                    throw NestedAccessorException::createAsCannotSetValue($mode, implode($this->pathDelimiter, $path));
276
                }
277
                $temp = &$temp->{$key};
278
            } else {
279
                // TODO check PHPStan: "Cannot access offset string on mixed"
280
                /** @var array<string, mixed> $temp */
281
                $temp = &$temp[$key];
282
            }
283
        }
284
        // now we can save value to the source
285
        switch($mode) {
286
            case self::SET_MODE_SET:
287
                $temp = $value;
288
                break;
289
            case self::SET_MODE_APPEND:
290
                $this->_append($temp, $value, $path, $strict);
291
                break;
292
            case self::SET_MODE_DELETE:
293
                if(!$this->_delete($tempPrevSource, $tempPrevKey, $path, $strict)) {
294
                    return $this;
295
                }
296
                break;
297
        }
298
        unset($temp);
299
300
        return $this;
301
    }
302
303
    /**
304
     * Appends value to source.
305
     * @param mixed $source source to append value to
306
     * @param mixed $value value to append to source
307
     * @param array<string> $path nested path
308
     * @param bool $strict if true: throw exception when cannot append value
309
     * @return void
310
     * @throws NestedAccessorException if cannot append item to source (in strict mode only)
311
     */
312
    protected function _append(&$source, $value, array $path, bool $strict): void
313
    {
314
        if(!is_array($source) || ArrayHelper::isAssoc($source)) {
315
            if($strict) {
316
                throw NestedAccessorException::createAsCannotSetValue(
317
                    self::SET_MODE_APPEND,
318
                    implode($this->pathDelimiter, $path)
319
                );
320
            } elseif(!is_array($source)) {
321
                $source = [];
322
            }
323
        }
324
325
        $source[] = $value;
326
    }
327
328
    /**
329
     * Removes item from source.
330
     * @param mixed $source source to remove item from
331
     * @param string|null $key key to remove item from source by
332
     * @param array<string> $path nested path
333
     * @param bool $strict if true: throw exception when cannot remove item
334
     * @return bool true if removing is succeeded
335
     * @throws NestedAccessorException if item to remove is not exists (in strict mode only)
336
     */
337
    protected function _delete(&$source, $key, array $path, bool $strict): bool
338
    {
339
        if($key === null || (!is_array($source) && !($source instanceof stdClass))) {
340
            if($strict) {
341
                throw NestedAccessorException::createAsCannotSetValue(
342
                    self::SET_MODE_DELETE,
343
                    implode($this->pathDelimiter, $path)
344
                );
345
            } else {
346
                return false;
347
            }
348
        }
349
        if(is_array($source)) {
350
            unset($source[$key]);
351
        } else {
352
            unset($source->{$key});
353
        }
354
355
        return true;
356
    }
357
358
    /**
359
     * @param string|string[]|null $path
360
     * @return string[]
361
     */
362
    protected function formatPath($path): array
363
    {
364
        if(is_array($path)) {
365
            return $path;
366
        }
367
368
        if($path === null || $path === '') {
369
            return [];
370
        }
371
372
        return explode($this->pathDelimiter, $path);
373
    }
374
375
    /**
376
     * @param array<string> $path
377
     * @return bool
378
     */
379
    protected function isLastKeyInteger(array $path): bool
380
    {
381
        $lastKey = $path[count($path)-1];
382
        return boolval(preg_match('/^[0-9]+$/', $lastKey));
383
    }
384
}
385