Passed
Push — master ( 67238a...43c1ab )
by Smoren
12:10
created

NestedAccessor::_set()   C

Complexity

Conditions 12
Paths 27

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 21
c 1
b 0
f 0
nc 27
nop 5
dl 0
loc 39
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
9
/**
10
 * Accessor class for getting and setting to source array or object with nested keys
11
 * @author Smoren <[email protected]>
12
 */
13
class NestedAccessor implements NestedAccessorInterface
14
{
15
    /**
16
     * @var array<int|string, mixed>|object data source for accessing
17
     */
18
    protected $source;
19
    /**
20
     * @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...
21
     */
22
    protected string $pathDelimiter;
23
24
    /**
25
     * ArrayNestedAccessor constructor.
26
     * @param array<scalar, mixed>|object $source
27
     * @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...
28
     * @throws NestedAccessorException
29
     */
30
    public function __construct(&$source, string $pathDelimiter = '.')
31
    {
32
        $this->setSource($source);
33
        $this->pathDelimiter = $pathDelimiter;
34
    }
35
36
    /**
37
     * Setter for source
38
     * @param array<scalar, mixed>|object $source source setter
39
     * @return void
40
     * @throws NestedAccessorException
41
     */
42
    public function setSource(&$source): void
43
    {
44
        /** @var array<scalar, mixed>|object|mixed|null $source */
45
        if($source === null) {
46
            $source = [];
47
        }
48
49
        if(is_scalar($source)) {
50
            throw NestedAccessorException::createAsSourceIsScalar($source);
51
        }
52
53
        /** @var array<int|string, mixed>|object $source */
54
        $this->source = &$source;
55
    }
56
57
    /**
58
     * Getter of source part specified by nested path
59
     * @param string|array<string>|null $path nested path
60
     * @param bool $strict if true: throw exception when path is not found in source
61
     * @return mixed value from source got by nested path
62
     * @throws NestedAccessorException if strict mode on and path is not found in source
63
     */
64
    public function get($path = null, bool $strict = true)
65
    {
66
        // when path is not specified
67
        if($path === null || $path === '') {
68
            // let's return the full source
69
            return $this->source;
70
        }
71
72
        $path = $this->formatPath($path);
73
74
        // let result be null and there are no errors by default
75
        $result = null;
76
        $errorsCount = 0;
77
78
        // getting result with internal recursive method
79
        $this->_get(
80
            $this->source,
81
            array_reverse($path), // path stack
82
            $result,
83
            $errorsCount
84
        );
85
86
        // when strict mode is on and we got errors
87
        if($strict && $errorsCount) {
88
            throw NestedAccessorException::createAsCannotGetValue(
89
                implode($this->pathDelimiter, $path),
90
                $errorsCount
91
            );
92
        }
93
94
        return $result;
95
    }
96
97
    /**
98
     * Setter of source part specified by nested path
99
     * @param string|array<string> $path nested path
100
     * @param mixed $value value to save by path
101
     * @param bool $strict when true throw exception if path not exist in source object
102
     * @return $this
103
     * @throws NestedAccessorException
104
     */
105
    public function set($path, $value, bool $strict = true): self
106
    {
107
        $path = $this->formatPath($path);
108
        return $this->_set($this->source, $path, $value, false, $strict);
109
    }
110
111
    /**
112
     * Appender of source part specified by nested path
113
     * @param string|array<string> $path nested path
114
     * @param mixed $value value to save by path
115
     * @param bool $strict when true throw exception if path not exist in source object
116
     * @return $this
117
     * @throws NestedAccessorException
118
     */
119
    public function append($path, $value, bool $strict = true): self
120
    {
121
        $path = $this->formatPath($path);
122
        return $this->_set($this->source, $path, $value, true, $strict);
123
    }
124
125
    /**
126
     * Internal recursive method to get value from source by path stack
127
     * @param mixed $source source to get value from
128
     * @param array<string> $path nested path stack
129
     * @param array<scalar, mixed>|mixed $result place for result
130
     * @param int $errorsCount errors counter
131
     * @return void
132
     */
133
    protected function _get($source, array $path, &$result, int &$errorsCount): void
134
    {
135
        // let's iterate every path part from stack
136
        while(count($path)) {
137
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
138
                // the result will be multiple
139
                if(!is_array($result)) {
140
                    $result = [];
141
                }
142
                // and we need to use recursive call for each item of this array
143
                foreach($source as $item) {
144
                    $this->_get($item, $path, $result, $errorsCount);
145
                }
146
                // we don't need to do something in this recursive branch
147
                return;
148
            }
149
150
            $key = array_pop($path);
151
152
            if(is_array($source)) {
153
                if(!array_key_exists($key, $source)) {
154
                    // path part key is missing in source array
155
                    $errorsCount++;
156
                    // we cannot go deeper
157
                    return;
158
                }
159
                // go to the next nested level
160
                $source = $source[$key];
161
            } elseif(is_object($source)) {
162
                $getterName = 'get'.ucfirst($key);
163
                if(method_exists($source, $getterName)) {
164
                    // go to the next nested level
165
                    $source = $source->{$getterName}();
166
                } elseif(property_exists($source, $key)) {
167
                    // go to the next nested level
168
                    $source = $source->{$key};
169
                } else {
170
                    // path part key is missing in source object
171
                    $errorsCount++;
172
                    // we cannot go deeper
173
                    return;
174
                }
175
            } else {
176
                // source is scalar, so we can't go to the next depth level
177
                $errorsCount++;
178
                // we cannot go deeper
179
                return;
180
            }
181
182
            // when it's not the last iteration of the stack
183
            // and the source is non-associative array (list)
184
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
185
                // the result will be multiple
186
                if(!is_array($result)) {
187
                    $result = [];
188
                }
189
                // and we need to use recursive call for each item of this array
190
                foreach($source as $item) {
191
                    $this->_get($item, $path, $result, $errorsCount);
192
                }
193
                // we don't need to do something in this recursive branch
194
                return;
195
            }
196
        }
197
198
        // now path stack is empty — we reached target value of given path in source argument
199
        // so if result is multiple
200
        if(is_array($result)) {
201
            // we append source to result
202
            $result[] = $source;
203
        } else {
204
            // result is single
205
            $result = $source;
206
        }
207
        // that's all folks!
208
    }
209
210
    /**
211
     * Internal recursive method to save value to source by path stack
212
     * @param array<scalar, mixed>|object $source source to save value to
213
     * @param array<string> $path nested path
214
     * @param mixed $value value to save to source
215
     * @param bool $append when true append or set
216
     * @param bool $strict when true throw exception if path not exist in source object
217
     * @return $this
218
     * @throws NestedAccessorException
219
     */
220
    protected function _set(&$source, array $path, $value, bool $append, bool $strict): self
221
    {
222
        $temp = &$source;
223
        // let's iterate every path part to go deeper into nesting
224
        foreach($path as $key) {
225
            if(isset($temp) && is_scalar($temp)) {
226
                // value in the middle of the path must me an array
227
                $temp = [];
228
            }
229
230
            // go to the next nested level
231
            if(is_object($temp)) {
232
                if($strict && !property_exists($temp, $key)) {
233
                    throw NestedAccessorException::createAsCannotSetValue(implode($this->pathDelimiter, $path));
234
                }
235
                $temp = &$temp->{$key};
236
            } else {
237
                // TODO check PHPStan: "Cannot access offset string on mixed"
238
                /** @var array<string, mixed> $temp */
239
                $temp = &$temp[$key];
240
            }
241
        }
242
        // now we can save value to the source
243
        if($append) {
244
            if(!is_array($temp) || ArrayHelper::isAssoc($temp)) {
245
                if($strict) {
246
                    throw NestedAccessorException::createAsCannotSetValue(implode($this->pathDelimiter, $path));
247
                } elseif(!is_array($temp)) {
248
                    $temp = [];
249
                }
250
            }
251
252
            $temp[] = $value;
253
        } else {
254
            $temp = $value;
255
        }
256
        unset($temp);
257
258
        return $this;
259
    }
260
261
    /**
262
     * @param string|string[]|null $path
263
     * @return string[]
264
     */
265
    protected function formatPath($path): array
266
    {
267
        if(is_array($path)) {
268
            return $path;
269
        }
270
271
        if($path === null || $path === '') {
272
            return [];
273
        }
274
275
        return explode($this->pathDelimiter, $path);
276
    }
277
}
278