Passed
Push — master ( 1fd154...475181 )
by Smoren
01:40
created

NestedAccessor   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 80
c 1
b 0
f 0
dl 0
loc 230
rs 9.44
wmc 37

6 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 6 2
A setSource() 0 13 3
A __construct() 0 4 1
A get() 0 33 6
B _set() 0 27 7
D _get() 0 82 18
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
        if(!is_array($path)) {
73
            $path = explode($this->pathDelimiter, $path);
74
        }
75
76
        // let result be null and there are no errors by default
77
        $result = null;
78
        $errorsCount = 0;
79
80
        // getting result with internal recursive method
81
        $this->_get(
82
            $this->source,
83
            array_reverse($path), // path stack
84
            $result,
85
            $errorsCount
86
        );
87
88
        // when strict mode is on and we got errors
89
        if($strict && $errorsCount) {
90
            throw NestedAccessorException::createAsCannotGetValue(
91
                implode($this->pathDelimiter, $path),
92
                $errorsCount
93
            );
94
        }
95
96
        return $result;
97
    }
98
99
    /**
100
     * Setter of source part specified by nested path
101
     * @param string|array<string> $path nested path
102
     * @param mixed $value value to save by path
103
     * @param bool $strict when true throw exception if path not exist in source object
104
     * @return $this
105
     * @throws NestedAccessorException
106
     */
107
    public function set($path, $value, bool $strict = true): self
108
    {
109
        if(!is_array($path)) {
110
            $path = explode($this->pathDelimiter, $path);
111
        }
112
        return $this->_set($this->source, $path, $value, $strict);
113
    }
114
115
    /**
116
     * Internal recursive method to get value from source by path stack
117
     * @param mixed $source source to get value from
118
     * @param array<string> $path nested path stack
119
     * @param array<scalar, mixed>|mixed $result place for result
120
     * @param int $errorsCount errors counter
121
     * @return void
122
     */
123
    protected function _get($source, array $path, &$result, int &$errorsCount): void
124
    {
125
        // if path stack is empty — we reached target value of given path in source argument
126
        if(!count($path)) {
127
            // so if result is multiple
128
            if(is_array($result)) {
129
                // we append source to result
130
                $result[] = $source;
131
            } else {
132
                // result is single
133
                $result = $source;
134
            }
135
            // we don't need to do something in this recursive branch
136
            return;
137
        }
138
139
        // let's iterate every path part from stack
140
        while(count($path)) {
141
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
142
                // the result will be multiple
143
                if(!is_array($result)) {
144
                    $result = [];
145
                }
146
                // and we need to use recursive call for each item of this array
147
                foreach($source as $item) {
148
                    $this->_get($item, $path, $result, $errorsCount);
149
                }
150
                // we don't need to do something in this recursive branch
151
                return;
152
            }
153
154
            $key = array_pop($path);
155
156
            if(is_array($source)) {
157
                if(!array_key_exists($key, $source)) {
158
                    // path part key is missing in source array
159
                    $errorsCount++;
160
                    // we cannot go deeper
161
                    return;
162
                }
163
                // go to the next nested level
164
                $source = $source[$key];
165
            } elseif(is_object($source)) {
166
                $getterName = 'get'.ucfirst($key);
167
                if(method_exists($source, $getterName)) {
168
                    // go to the next nested level
169
                    $source = $source->{$getterName}();
170
                } elseif(property_exists($source, $key)) {
171
                    // go to the next nested level
172
                    $source = $source->{$key};
173
                } else {
174
                    // path part key is missing in source object
175
                    $errorsCount++;
176
                    // we cannot go deeper
177
                    return;
178
                }
179
            } else {
180
                // source is scalar, so we can't go to the next depth level
181
                $errorsCount++;
182
                // we cannot go deeper
183
                return;
184
            }
185
186
            // when it's not the last iteration of the stack
187
            // and the source is non-associative array (list)
188
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
189
                // the result will be multiple
190
                if(!is_array($result)) {
191
                    $result = [];
192
                }
193
                // and we need to use recursive call for each item of this array
194
                foreach($source as $item) {
195
                    $this->_get($item, $path, $result, $errorsCount);
196
                }
197
                // we don't need to do something in this recursive branch
198
                return;
199
            }
200
        }
201
202
        // if all the path successfully passed
203
        // we need only one recursive call to save source to result
204
        $this->_get($source, $path, $result, $errorsCount);
205
    }
206
207
    /**
208
     * Internal recursive method to save value to source by path stack
209
     * @param array<scalar, mixed>|object $source source to save value to
210
     * @param array<string> $path nested path
211
     * @param mixed $value value to save to source
212
     * @param bool $strict when true throw exception if path not exist in source object
213
     * @return $this
214
     * @throws NestedAccessorException
215
     */
216
    protected function _set(&$source, array $path, $value, bool $strict): self
217
    {
218
        $temp = &$source;
219
        // let's iterate every path part to go deeper into nesting
220
        foreach($path as $key) {
221
            if(isset($temp) && is_scalar($temp)) {
222
                // value in the middle of the path must me an array
223
                $temp = [];
224
            }
225
226
            // go to the next nested level
227
            if(is_object($temp)) {
228
                if($strict && !property_exists($temp, $key)) {
229
                    throw NestedAccessorException::createAsCannotSetValue($key);
230
                }
231
                $temp = &$temp->{$key};
232
            } else {
233
                // TODO check PHPStan: "Cannot access offset string on mixed"
234
                /** @var array<string, mixed> $temp */
235
                $temp = &$temp[$key];
236
            }
237
        }
238
        // now we can save value to the source
239
        $temp = $value;
240
        unset($temp);
241
242
        return $this;
243
    }
244
}
245