Passed
Push — master ( 78936a...47c5f2 )
by Smoren
02:13
created

NestedAccessor::exist()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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
     * {@inheritDoc}
59
     */
60
    public function get($path = null, bool $strict = true)
61
    {
62
        // when path is not specified
63
        if($path === null || $path === '') {
64
            // let's return the full source
65
            return $this->source;
66
        }
67
68
        $path = $this->formatPath($path);
69
70
        // let result be null and there are no errors by default
71
        $result = null;
72
        $errorsCount = 0;
73
74
        // getting result with internal recursive method
75
        $this->_get(
76
            $this->source,
77
            array_reverse($path), // path stack
78
            $result,
79
            $errorsCount
80
        );
81
82
        // when strict mode is on and we got errors
83
        if($strict && $errorsCount) {
84
            throw NestedAccessorException::createAsCannotGetValue(
85
                implode($this->pathDelimiter, $path),
86
                $errorsCount
87
            );
88
        }
89
90
        return $result;
91
    }
92
93
    /**
94
     * {@inheritDoc}
95
     */
96
    public function set($path, $value, bool $strict = true): self
97
    {
98
        $path = $this->formatPath($path);
99
        return $this->_set($this->source, $path, $value, false, $strict);
100
    }
101
102
    /**
103
     * {@inheritDoc}
104
     */
105
    public function append($path, $value, bool $strict = true): self
106
    {
107
        $path = $this->formatPath($path);
108
        return $this->_set($this->source, $path, $value, true, $strict);
109
    }
110
111
    /**
112
     * {@inheritDoc}
113
     */
114
    public function exist($path): bool
115
    {
116
        try {
117
            $this->get($path);
118
            return true;
119
        } catch(NestedAccessorException $e) {
120
            return false;
121
        }
122
    }
123
124
    /**
125
     * {@inheritDoc}
126
     */
127
    public function isset($path): bool
128
    {
129
        try {
130
            return $this->get($path) !== null;
131
        } catch(NestedAccessorException $e) {
132
            return false;
133
        }
134
    }
135
136
    /**
137
     * Internal recursive method to get value from source by path stack
138
     * @param mixed $source source to get value from
139
     * @param array<string> $path nested path stack
140
     * @param array<scalar, mixed>|mixed $result place for result
141
     * @param int $errorsCount errors counter
142
     * @return void
143
     */
144
    protected function _get($source, array $path, &$result, int &$errorsCount): void
145
    {
146
        // let's iterate every path part from stack
147
        while(count($path)) {
148
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
149
                // the result will be multiple
150
                if(!is_array($result)) {
151
                    $result = [];
152
                }
153
                // and we need to use recursive call for each item of this array
154
                foreach($source as $item) {
155
                    $this->_get($item, $path, $result, $errorsCount);
156
                }
157
                // we don't need to do something in this recursive branch
158
                return;
159
            }
160
161
            $key = array_pop($path);
162
163
            if(is_array($source)) {
164
                if(!array_key_exists($key, $source)) {
165
                    // path part key is missing in source array
166
                    $errorsCount++;
167
                    // we cannot go deeper
168
                    return;
169
                }
170
                // go to the next nested level
171
                $source = $source[$key];
172
            } elseif(is_object($source)) {
173
                $getterName = 'get'.ucfirst($key);
174
                if(method_exists($source, $getterName)) {
175
                    // go to the next nested level
176
                    $source = $source->{$getterName}();
177
                } elseif(property_exists($source, $key)) {
178
                    // go to the next nested level
179
                    $source = $source->{$key};
180
                } else {
181
                    // path part key is missing in source object
182
                    $errorsCount++;
183
                    // we cannot go deeper
184
                    return;
185
                }
186
            } else {
187
                // source is scalar, so we can't go to the next depth level
188
                $errorsCount++;
189
                // we cannot go deeper
190
                return;
191
            }
192
193
            // when it's not the last iteration of the stack
194
            // and the source is non-associative array (list)
195
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
196
                // the result will be multiple
197
                if(!is_array($result)) {
198
                    $result = [];
199
                }
200
                // and we need to use recursive call for each item of this array
201
                foreach($source as $item) {
202
                    $this->_get($item, $path, $result, $errorsCount);
203
                }
204
                // we don't need to do something in this recursive branch
205
                return;
206
            }
207
        }
208
209
        // now path stack is empty — we reached target value of given path in source argument
210
        // so if result is multiple
211
        if(is_array($result)) {
212
            // we append source to result
213
            $result[] = $source;
214
        } else {
215
            // result is single
216
            $result = $source;
217
        }
218
        // that's all folks!
219
    }
220
221
    /**
222
     * Internal recursive method to save value to source by path stack
223
     * @param array<scalar, mixed>|object $source source to save value to
224
     * @param array<string> $path nested path
225
     * @param mixed $value value to save to source
226
     * @param bool $append when true append or set
227
     * @param bool $strict when true throw exception if path not exist in source object
228
     * @return $this
229
     * @throws NestedAccessorException
230
     */
231
    protected function _set(&$source, array $path, $value, bool $append, bool $strict): self
232
    {
233
        $temp = &$source;
234
        // let's iterate every path part to go deeper into nesting
235
        foreach($path as $key) {
236
            if(isset($temp) && is_scalar($temp)) {
237
                // value in the middle of the path must me an array
238
                $temp = [];
239
            }
240
241
            // go to the next nested level
242
            if(is_object($temp)) {
243
                if($strict && !property_exists($temp, $key)) {
244
                    throw NestedAccessorException::createAsCannotSetValue(implode($this->pathDelimiter, $path));
245
                }
246
                $temp = &$temp->{$key};
247
            } else {
248
                // TODO check PHPStan: "Cannot access offset string on mixed"
249
                /** @var array<string, mixed> $temp */
250
                $temp = &$temp[$key];
251
            }
252
        }
253
        // now we can save value to the source
254
        if($append) {
255
            if(!is_array($temp) || ArrayHelper::isAssoc($temp)) {
256
                if($strict) {
257
                    throw NestedAccessorException::createAsCannotSetValue(implode($this->pathDelimiter, $path));
258
                } elseif(!is_array($temp)) {
259
                    $temp = [];
260
                }
261
            }
262
263
            $temp[] = $value;
264
        } else {
265
            $temp = $value;
266
        }
267
        unset($temp);
268
269
        return $this;
270
    }
271
272
    /**
273
     * @param string|string[]|null $path
274
     * @return string[]
275
     */
276
    protected function formatPath($path): array
277
    {
278
        if(is_array($path)) {
279
            return $path;
280
        }
281
282
        if($path === null || $path === '') {
283
            return [];
284
        }
285
286
        return explode($this->pathDelimiter, $path);
287
    }
288
}
289