Passed
Push — master ( 82a2cd...181ad7 )
by Smoren
11:41
created

NestedAccessor   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 121
c 2
b 0
f 0
dl 0
loc 308
rs 6
wmc 55

11 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 4 1
A formatPath() 0 11 4
A setSource() 0 13 3
D _set() 0 68 20
C _get() 0 54 13
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

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\Helpers\KeyAccessHelper;
7
use Smoren\NestedAccessor\Helpers\ObjectHelper;
8
use Smoren\NestedAccessor\Interfaces\NestedAccessorInterface;
9
use Smoren\NestedAccessor\Exceptions\NestedAccessorException;
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) && !ArrayHelper::isAssoc($source)) {
176
                // the result will be multiple
177
                if(!is_array($result)) {
178
                    $result = [];
179
                }
180
                // and we need to use recursive call for each item of this array
181
                foreach($source as $item) {
182
                    $this->_get($item, $path, $result, $errorsCount);
183
                }
184
                // we don't need to do something in this recursive branch
185
                return;
186
            }
187
188
            $key = array_pop($path);
189
190
            if(KeyAccessHelper::exists($source, $key)) {
191
                // go to the next nested level
192
                $source = KeyAccessHelper::get($source, $key);
193
            } else {
194
                // path part key is missing in source object
195
                $errorsCount++;
196
                // we cannot go deeper
197
                return;
198
            }
199
200
            // when it's not the last iteration of the stack
201
            // and the source is non-associative array (list)
202
            /** @var mixed $source */
203
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
204
                // the result will be multiple
205
                if(!is_array($result)) {
206
                    $result = [];
207
                }
208
                // and we need to use recursive call for each item of this array
209
                foreach($source as $item) {
210
                    $this->_get($item, $path, $result, $errorsCount);
211
                }
212
                // we don't need to do something in this recursive branch
213
                return;
214
            }
215
        }
216
217
        // now path stack is empty — we reached target value of given path in source argument
218
        // so if result is multiple
219
        if(is_array($result)) {
220
            // we append source to result
221
            $result[] = $source;
222
        } else {
223
            // result is single
224
            $result = $source;
225
        }
226
        // that's all folks!
227
    }
228
229
    /**
230
     * Internal recursive method to save value to source by path stack
231
     * @param array<scalar, mixed>|object $source source to save value to
232
     * @param array<string> $path nested path
233
     * @param mixed $value value to save to source
234
     * @param int $mode when true append or set
235
     * @param bool $strict when true throw exception if path not exist in source object
236
     * @return $this
237
     * @throws NestedAccessorException
238
     */
239
    protected function _set(&$source, array $path, $value, int $mode, bool $strict): self
240
    {
241
        $temp = &$source;
242
        $tempPrevSource = null;
243
        $tempPrevKey = null;
244
245
        // let's iterate every path part to go deeper into nesting
246
        foreach($path as $key) {
247
            if(isset($temp) && is_scalar($temp)) {
248
                // value in the middle of the path must be an array
249
                $temp = [];
250
            }
251
252
            $tempPrevSource = &$temp;
253
            $tempPrevKey = $key;
254
255
            // go to the next nested level
256
            if(is_object($temp)) {
257
                if($strict && !($temp instanceof stdClass) && !ObjectHelper::hasPublicProperty($temp, $key)) {
258
                    throw NestedAccessorException::createAsCannotSetValue($mode, implode($this->pathDelimiter, $path));
259
                }
260
                $temp = &$temp->{$key};
261
            } else {
262
                // TODO check PHPStan: "Cannot access offset string on mixed"
263
                /** @var array<string, mixed> $temp */
264
                $temp = &$temp[$key];
265
            }
266
        }
267
        // now we can save value to the source
268
        switch($mode) {
269
            case self::SET_MODE_SET:
270
                $temp = $value;
271
                break;
272
            case self::SET_MODE_APPEND:
273
                if(!is_array($temp) || ArrayHelper::isAssoc($temp)) {
274
                    if($strict) {
275
                        throw NestedAccessorException::createAsCannotSetValue(
276
                            $mode,
277
                            implode($this->pathDelimiter, $path)
278
                        );
279
                    } elseif(!is_array($temp)) {
280
                        $temp = [];
281
                    }
282
                }
283
284
                $temp[] = $value;
285
                break;
286
            case self::SET_MODE_DELETE:
287
                if($tempPrevKey === null || (!is_array($tempPrevSource) && !($tempPrevSource instanceof stdClass))) {
288
                    if($strict) {
289
                        throw NestedAccessorException::createAsCannotSetValue(
290
                            $mode,
291
                            implode($this->pathDelimiter, $path)
292
                        );
293
                    } else {
294
                        return $this;
295
                    }
296
                }
297
                if(is_array($tempPrevSource)) {
0 ignored issues
show
introduced by
The condition is_array($tempPrevSource) is always true.
Loading history...
298
                    unset($tempPrevSource[$tempPrevKey]);
299
                } else {
300
                    unset($tempPrevSource->{$tempPrevKey});
301
                }
302
                break;
303
        }
304
        unset($temp);
305
306
        return $this;
307
    }
308
309
    /**
310
     * @param string|string[]|null $path
311
     * @return string[]
312
     */
313
    protected function formatPath($path): array
314
    {
315
        if(is_array($path)) {
316
            return $path;
317
        }
318
319
        if($path === null || $path === '') {
320
            return [];
321
        }
322
323
        return explode($this->pathDelimiter, $path);
324
    }
325
}
326