Passed
Push — master ( 72f38a...596bea )
by Smoren
02:21
created

NestedAccessor::formatPath()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 10
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 stdClass;
9
10
/**
11
 * Accessor class for getting and setting to source array or object with nested keys
12
 * @author Smoren <[email protected]>
13
 */
14
class NestedAccessor implements NestedAccessorInterface
15
{
16
    public const SET_MODE_SET = 1;
17
    public const SET_MODE_APPEND = 2;
18
    public const SET_MODE_DELETE = 3;
19
20
    /**
21
     * @var array<int|string, mixed>|object data source for accessing
22
     */
23
    protected $source;
24
    /**
25
     * @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...
26
     */
27
    protected string $pathDelimiter;
28
29
    /**
30
     * ArrayNestedAccessor constructor.
31
     * @param array<scalar, 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
     * @throws NestedAccessorException
34
     */
35
    public function __construct(&$source, string $pathDelimiter = '.')
36
    {
37
        $this->setSource($source);
38
        $this->pathDelimiter = $pathDelimiter;
39
    }
40
41
    /**
42
     * Setter for source
43
     * @param array<scalar, mixed>|object $source source setter
44
     * @return void
45
     * @throws NestedAccessorException
46
     */
47
    public function setSource(&$source): void
48
    {
49
        /** @var array<scalar, mixed>|object|mixed|null $source */
50
        if($source === null) {
51
            $source = [];
52
        }
53
54
        if(is_scalar($source)) {
55
            throw NestedAccessorException::createAsSourceIsScalar($source);
56
        }
57
58
        /** @var array<int|string, mixed>|object $source */
59
        $this->source = &$source;
60
    }
61
62
    /**
63
     * {@inheritDoc}
64
     */
65
    public function get($path = null, bool $strict = true)
66
    {
67
        // when path is not specified
68
        if($path === null || $path === '') {
69
            // let's return the full source
70
            return $this->source;
71
        }
72
73
        $path = $this->formatPath($path);
74
75
        // let result be null and there are no errors by default
76
        $result = null;
77
        $errorsCount = 0;
78
79
        // getting result with internal recursive method
80
        $this->_get(
81
            $this->source,
82
            array_reverse($path), // path stack
83
            $result,
84
            $errorsCount
85
        );
86
87
        // when strict mode is on and we got errors
88
        if($strict && $errorsCount) {
89
            throw NestedAccessorException::createAsCannotGetValue(
90
                implode($this->pathDelimiter, $path),
91
                $errorsCount
92
            );
93
        }
94
95
        return $result;
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     */
101
    public function set($path, $value, bool $strict = true): self
102
    {
103
        $path = $this->formatPath($path);
104
        return $this->_set($this->source, $path, $value, self::SET_MODE_SET, $strict);
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     */
110
    public function append($path, $value, bool $strict = true): self
111
    {
112
        $path = $this->formatPath($path);
113
        return $this->_set($this->source, $path, $value, self::SET_MODE_APPEND, $strict);
114
    }
115
116
    /**
117
     * {@inheritDoc}
118
     */
119
    public function delete($path, bool $strict = true): self
120
    {
121
        $path = $this->formatPath($path);
122
123
        if(!$this->exist($path)) {
124
            if($strict) {
125
                throw NestedAccessorException::createAsCannotSetValue(
126
                    self::SET_MODE_DELETE,
127
                    implode($this->pathDelimiter, $path)
128
                );
129
            }
130
            return $this;
131
        }
132
133
        return $this->_set($this->source, $path, null, self::SET_MODE_DELETE, $strict);
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     */
139
    public function exist($path): bool
140
    {
141
        try {
142
            $this->get($path);
143
            return true;
144
        } catch(NestedAccessorException $e) {
145
            return false;
146
        }
147
    }
148
149
    /**
150
     * {@inheritDoc}
151
     */
152
    public function isset($path): bool
153
    {
154
        try {
155
            return $this->get($path) !== null;
156
        } catch(NestedAccessorException $e) {
157
            return false;
158
        }
159
    }
160
161
    /**
162
     * Internal recursive method to get value from source by path stack
163
     * @param mixed $source source to get value from
164
     * @param array<string> $path nested path stack
165
     * @param array<scalar, mixed>|mixed $result place for result
166
     * @param int $errorsCount errors counter
167
     * @return void
168
     */
169
    protected function _get($source, array $path, &$result, int &$errorsCount): void
170
    {
171
        // let's iterate every path part from stack
172
        while(count($path)) {
173
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
174
                // the result will be multiple
175
                if(!is_array($result)) {
176
                    $result = [];
177
                }
178
                // and we need to use recursive call for each item of this array
179
                foreach($source as $item) {
180
                    $this->_get($item, $path, $result, $errorsCount);
181
                }
182
                // we don't need to do something in this recursive branch
183
                return;
184
            }
185
186
            $key = array_pop($path);
187
188
            if(is_array($source)) {
189
                if(!array_key_exists($key, $source)) {
190
                    // path part key is missing in source array
191
                    $errorsCount++;
192
                    // we cannot go deeper
193
                    return;
194
                }
195
                // go to the next nested level
196
                $source = $source[$key];
197
            } elseif(is_object($source)) {
198
                $getterName = 'get'.ucfirst($key);
199
                if(method_exists($source, $getterName)) {
200
                    // go to the next nested level
201
                    $source = $source->{$getterName}();
202
                } elseif(property_exists($source, $key)) {
203
                    // go to the next nested level
204
                    $source = $source->{$key};
205
                } else {
206
                    // path part key is missing in source object
207
                    $errorsCount++;
208
                    // we cannot go deeper
209
                    return;
210
                }
211
            } else {
212
                // source is scalar, so we can't go to the next depth level
213
                $errorsCount++;
214
                // we cannot go deeper
215
                return;
216
            }
217
218
            // when it's not the last iteration of the stack
219
            // and the source is non-associative array (list)
220
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
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 && !property_exists($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
                if(!is_array($temp) || ArrayHelper::isAssoc($temp)) {
291
                    if($strict) {
292
                        throw NestedAccessorException::createAsCannotSetValue(
293
                            $mode,
294
                            implode($this->pathDelimiter, $path)
295
                        );
296
                    } elseif(!is_array($temp)) {
297
                        $temp = [];
298
                    }
299
                }
300
301
                $temp[] = $value;
302
                break;
303
            case self::SET_MODE_DELETE:
304
                if($tempPrevKey === null || (!is_array($tempPrevSource) && !($tempPrevSource instanceof stdClass))) {
305
                    if($strict) {
306
                        throw NestedAccessorException::createAsCannotSetValue(
307
                            $mode,
308
                            implode($this->pathDelimiter, $path)
309
                        );
310
                    } else {
311
                        return $this;
312
                    }
313
                }
314
                if(is_array($tempPrevSource)) {
0 ignored issues
show
introduced by
The condition is_array($tempPrevSource) is always true.
Loading history...
315
                    unset($tempPrevSource[$tempPrevKey]);
316
                } else {
317
                    unset($tempPrevSource->{$tempPrevKey});
318
                }
319
                break;
320
        }
321
        unset($temp);
322
323
        return $this;
324
    }
325
326
    /**
327
     * @param string|string[]|null $path
328
     * @return string[]
329
     */
330
    protected function formatPath($path): array
331
    {
332
        if(is_array($path)) {
333
            return $path;
334
        }
335
336
        if($path === null || $path === '') {
337
            return [];
338
        }
339
340
        return explode($this->pathDelimiter, $path);
341
    }
342
}
343