Passed
Push — master ( cdc2ae...72f38a )
by Smoren
02:30
created

NestedAccessor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 4
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
        if(!$this->exist($path)) {
122
            if($strict) {
123
                throw NestedAccessorException::createAsCannotSetValue(self::SET_MODE_DELETE, $path);
124
            }
125
            return $this;
126
        }
127
128
        $path = $this->formatPath($path);
129
        return $this->_set($this->source, $path, null, self::SET_MODE_DELETE, $strict);
130
    }
131
132
    /**
133
     * {@inheritDoc}
134
     */
135
    public function exist($path): bool
136
    {
137
        try {
138
            $this->get($path);
139
            return true;
140
        } catch(NestedAccessorException $e) {
141
            return false;
142
        }
143
    }
144
145
    /**
146
     * {@inheritDoc}
147
     */
148
    public function isset($path): bool
149
    {
150
        try {
151
            return $this->get($path) !== null;
152
        } catch(NestedAccessorException $e) {
153
            return false;
154
        }
155
    }
156
157
    /**
158
     * Internal recursive method to get value from source by path stack
159
     * @param mixed $source source to get value from
160
     * @param array<string> $path nested path stack
161
     * @param array<scalar, mixed>|mixed $result place for result
162
     * @param int $errorsCount errors counter
163
     * @return void
164
     */
165
    protected function _get($source, array $path, &$result, int &$errorsCount): void
166
    {
167
        // let's iterate every path part from stack
168
        while(count($path)) {
169
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
170
                // the result will be multiple
171
                if(!is_array($result)) {
172
                    $result = [];
173
                }
174
                // and we need to use recursive call for each item of this array
175
                foreach($source as $item) {
176
                    $this->_get($item, $path, $result, $errorsCount);
177
                }
178
                // we don't need to do something in this recursive branch
179
                return;
180
            }
181
182
            $key = array_pop($path);
183
184
            if(is_array($source)) {
185
                if(!array_key_exists($key, $source)) {
186
                    // path part key is missing in source array
187
                    $errorsCount++;
188
                    // we cannot go deeper
189
                    return;
190
                }
191
                // go to the next nested level
192
                $source = $source[$key];
193
            } elseif(is_object($source)) {
194
                $getterName = 'get'.ucfirst($key);
195
                if(method_exists($source, $getterName)) {
196
                    // go to the next nested level
197
                    $source = $source->{$getterName}();
198
                } elseif(property_exists($source, $key)) {
199
                    // go to the next nested level
200
                    $source = $source->{$key};
201
                } else {
202
                    // path part key is missing in source object
203
                    $errorsCount++;
204
                    // we cannot go deeper
205
                    return;
206
                }
207
            } else {
208
                // source is scalar, so we can't go to the next depth level
209
                $errorsCount++;
210
                // we cannot go deeper
211
                return;
212
            }
213
214
            // when it's not the last iteration of the stack
215
            // and the source is non-associative array (list)
216
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
217
                // the result will be multiple
218
                if(!is_array($result)) {
219
                    $result = [];
220
                }
221
                // and we need to use recursive call for each item of this array
222
                foreach($source as $item) {
223
                    $this->_get($item, $path, $result, $errorsCount);
224
                }
225
                // we don't need to do something in this recursive branch
226
                return;
227
            }
228
        }
229
230
        // now path stack is empty — we reached target value of given path in source argument
231
        // so if result is multiple
232
        if(is_array($result)) {
233
            // we append source to result
234
            $result[] = $source;
235
        } else {
236
            // result is single
237
            $result = $source;
238
        }
239
        // that's all folks!
240
    }
241
242
    /**
243
     * Internal recursive method to save value to source by path stack
244
     * @param array<scalar, mixed>|object $source source to save value to
245
     * @param array<string> $path nested path
246
     * @param mixed $value value to save to source
247
     * @param int $mode when true append or set
248
     * @param bool $strict when true throw exception if path not exist in source object
249
     * @return $this
250
     * @throws NestedAccessorException
251
     */
252
    protected function _set(&$source, array $path, $value, int $mode, bool $strict): self
253
    {
254
        $temp = &$source;
255
        $tempPrevSource = null;
256
        $tempPrevKey = null;
257
258
        // let's iterate every path part to go deeper into nesting
259
        foreach($path as $key) {
260
            if(isset($temp) && is_scalar($temp)) {
261
                // value in the middle of the path must be an array
262
                $temp = [];
263
            }
264
265
            $tempPrevSource = &$temp;
266
            $tempPrevKey = $key;
267
268
            // go to the next nested level
269
            if(is_object($temp)) {
270
                if($strict && !property_exists($temp, $key)) {
271
                    throw NestedAccessorException::createAsCannotSetValue($mode, implode($this->pathDelimiter, $path));
272
                }
273
                $temp = &$temp->{$key};
274
            } else {
275
                // TODO check PHPStan: "Cannot access offset string on mixed"
276
                /** @var array<string, mixed> $temp */
277
                $temp = &$temp[$key];
278
            }
279
        }
280
        // now we can save value to the source
281
        switch($mode) {
282
            case self::SET_MODE_SET:
283
                $temp = $value;
284
                break;
285
            case self::SET_MODE_APPEND:
286
                if(!is_array($temp) || ArrayHelper::isAssoc($temp)) {
287
                    if($strict) {
288
                        throw NestedAccessorException::createAsCannotSetValue(
289
                            $mode,
290
                            implode($this->pathDelimiter, $path)
291
                        );
292
                    } elseif(!is_array($temp)) {
293
                        $temp = [];
294
                    }
295
                }
296
297
                $temp[] = $value;
298
                break;
299
            case self::SET_MODE_DELETE:
300
                if($tempPrevKey === null || (!is_array($tempPrevSource) && !($tempPrevSource instanceof stdClass))) {
301
                    throw NestedAccessorException::createAsCannotSetValue($mode, implode($this->pathDelimiter, $path));
302
                }
303
                if(is_array($tempPrevSource)) {
0 ignored issues
show
introduced by
The condition is_array($tempPrevSource) is always true.
Loading history...
304
                    unset($tempPrevSource[$tempPrevKey]);
305
                } else {
306
                    unset($tempPrevSource->{$tempPrevKey});
307
                }
308
                break;
309
        }
310
        unset($temp);
311
312
        return $this;
313
    }
314
315
    /**
316
     * @param string|string[]|null $path
317
     * @return string[]
318
     */
319
    protected function formatPath($path): array
320
    {
321
        if(is_array($path)) {
322
            return $path;
323
        }
324
325
        if($path === null || $path === '') {
326
            return [];
327
        }
328
329
        return explode($this->pathDelimiter, $path);
330
    }
331
}
332