Passed
Push — master ( 475181...67238a )
by Smoren
01:39
created

NestedAccessor   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 77
c 2
b 0
f 0
dl 0
loc 223
rs 9.52
wmc 36

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
C _get() 0 73 17
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
        // let's iterate every path part from stack
126
        while(count($path)) {
127
            if(is_array($source) && !ArrayHelper::isAssoc($source)) {
128
                // the result will be multiple
129
                if(!is_array($result)) {
130
                    $result = [];
131
                }
132
                // and we need to use recursive call for each item of this array
133
                foreach($source as $item) {
134
                    $this->_get($item, $path, $result, $errorsCount);
135
                }
136
                // we don't need to do something in this recursive branch
137
                return;
138
            }
139
140
            $key = array_pop($path);
141
142
            if(is_array($source)) {
143
                if(!array_key_exists($key, $source)) {
144
                    // path part key is missing in source array
145
                    $errorsCount++;
146
                    // we cannot go deeper
147
                    return;
148
                }
149
                // go to the next nested level
150
                $source = $source[$key];
151
            } elseif(is_object($source)) {
152
                $getterName = 'get'.ucfirst($key);
153
                if(method_exists($source, $getterName)) {
154
                    // go to the next nested level
155
                    $source = $source->{$getterName}();
156
                } elseif(property_exists($source, $key)) {
157
                    // go to the next nested level
158
                    $source = $source->{$key};
159
                } else {
160
                    // path part key is missing in source object
161
                    $errorsCount++;
162
                    // we cannot go deeper
163
                    return;
164
                }
165
            } else {
166
                // source is scalar, so we can't go to the next depth level
167
                $errorsCount++;
168
                // we cannot go deeper
169
                return;
170
            }
171
172
            // when it's not the last iteration of the stack
173
            // and the source is non-associative array (list)
174
            if(count($path) && is_array($source) && !ArrayHelper::isAssoc($source)) {
175
                // the result will be multiple
176
                if(!is_array($result)) {
177
                    $result = [];
178
                }
179
                // and we need to use recursive call for each item of this array
180
                foreach($source as $item) {
181
                    $this->_get($item, $path, $result, $errorsCount);
182
                }
183
                // we don't need to do something in this recursive branch
184
                return;
185
            }
186
        }
187
188
        // now path stack is empty — we reached target value of given path in source argument
189
        // so if result is multiple
190
        if(is_array($result)) {
191
            // we append source to result
192
            $result[] = $source;
193
        } else {
194
            // result is single
195
            $result = $source;
196
        }
197
        // that's all folks!
198
    }
199
200
    /**
201
     * Internal recursive method to save value to source by path stack
202
     * @param array<scalar, mixed>|object $source source to save value to
203
     * @param array<string> $path nested path
204
     * @param mixed $value value to save to source
205
     * @param bool $strict when true throw exception if path not exist in source object
206
     * @return $this
207
     * @throws NestedAccessorException
208
     */
209
    protected function _set(&$source, array $path, $value, bool $strict): self
210
    {
211
        $temp = &$source;
212
        // let's iterate every path part to go deeper into nesting
213
        foreach($path as $key) {
214
            if(isset($temp) && is_scalar($temp)) {
215
                // value in the middle of the path must me an array
216
                $temp = [];
217
            }
218
219
            // go to the next nested level
220
            if(is_object($temp)) {
221
                if($strict && !property_exists($temp, $key)) {
222
                    throw NestedAccessorException::createAsCannotSetValue($key);
223
                }
224
                $temp = &$temp->{$key};
225
            } else {
226
                // TODO check PHPStan: "Cannot access offset string on mixed"
227
                /** @var array<string, mixed> $temp */
228
                $temp = &$temp[$key];
229
            }
230
        }
231
        // now we can save value to the source
232
        $temp = $value;
233
        unset($temp);
234
235
        return $this;
236
    }
237
}
238