Passed
Pull Request — master (#7)
by Ashoka
06:53
created

DataContainer::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright MediaCT. All rights reserved.
4
 * https://www.mediact.nl
5
 */
6
7
namespace Mediact\DataContainer;
8
9
use ArrayIterator;
10
use Traversable;
11
12
/**
13
 * Contains any data which can be accessed using dot-notation.
14
 */
15
class DataContainer implements IterableDataContainerInterface
16
{
17
    use ReplaceByPatternTrait;
18
19
    /** @var array */
20
    private $data;
21
22
    /**
23
     * Constructor.
24
     *
25
     * @param array $data
26
     */
27 1
    public function __construct(array $data = [])
28
    {
29 1
        $this->data = $data;
30 1
    }
31
32
    /**
33
     * Check whether a path exists.
34
     *
35
     * @param string $path
36
     *
37
     * @return bool
38
     */
39 6
    public function has(string $path): bool
40
    {
41 6
        $check = clone $this;
42 6
        return $this->get($path, $check) !== $check;
43
    }
44
45
    /**
46
     * Get a value of a path.
47
     *
48
     * @param string $path
49
     * @param mixed  $default
50
     *
51
     * @return mixed
52
     */
53 6
    public function get(string $path, $default = null)
54
    {
55 6
        return array_reduce(
56 6
            $this->parsePath($path),
57
            function ($data, $key) use ($default) {
58 6
                return is_array($data) && array_key_exists($key, $data)
59 4
                    ? $data[$key]
60 6
                    : $default;
61 6
            },
62 6
            $this->data
63
        );
64
    }
65
66
    /**
67
     * Get the contained array.
68
     *
69
     * @return array
70
     */
71 1
    public function all(): array
72
    {
73 1
        return $this->data;
74
    }
75
76
    /**
77
     * Set a value on a path.
78
     *
79
     * @param string $path
80
     * @param mixed  $value
81
     *
82
     * @return void
83
     */
84 4
    public function set(string $path, $value = null)
85
    {
86 4
        $keys        = $this->parsePath($path);
87 4
        $last        = array_pop($keys);
88 4
        $node        =& $this->getNodeReference($keys);
89 4
        $node[$last] = $value;
90 4
    }
91
92
    /**
93
     * Remove a path if it exists.
94
     *
95
     * @param string $pattern
96
     *
97
     * @return void
98
     */
99 4
    public function remove(string $pattern)
100
    {
101 4
        foreach ($this->glob($pattern) as $path) {
102 3
            $keys = $this->parsePath($path);
103 3
            $last = array_pop($keys);
104 3
            $node =& $this->getNodeReference($keys);
105 3
            unset($node[$last]);
106
        }
107 4
    }
108
109
    /**
110
     * Find paths that match a pattern.
111
     *
112
     * @param string $pattern
113
     *
114
     * @return string[]
115
     */
116 7
    public function glob(string $pattern): array
117
    {
118 7
        return $this->findArrayPathsByPatterns(
119 7
            $this->data,
120 7
            explode(static::SEPARATOR, $pattern),
121 7
            ''
122
        );
123
    }
124
125
    /**
126
     * Find paths that match a pattern an their replacements.
127
     *
128
     * @param string $pattern
129
     * @param string $replacement
130
     *
131
     * @return string[]
132
     */
133 5
    public function expand(string $pattern, string $replacement): array
134
    {
135 5
        $matches = $this->glob($pattern);
136
137 5
        return array_combine(
138 5
            $matches,
139 5
            array_map(
140
                function ($match) use ($pattern, $replacement) {
141 5
                    return $this->replaceByPattern(
142 5
                        $pattern,
143 5
                        $match,
144 5
                        $replacement,
145 5
                        static::SEPARATOR
146
                    );
147 5
                },
148 5
                $matches
149
            )
150
        );
151
    }
152
153
    /**
154
     * Branch into a list of data containers.
155
     *
156
     * @param string $pattern
157
     *
158
     * @return DataContainerInterface[]
159
     */
160 4
    public function branch(string $pattern): array
161
    {
162 4
        return array_map(
163
            function (array $data) : DataContainerInterface {
164 4
                return new static($data);
165 4
            },
166 4
            array_map(
167
                function (string $path) : array {
168 4
                    return (array) $this->get($path, []);
169 4
                },
170 4
                $this->glob($pattern)
171
            )
172
        );
173
    }
174
175
    /**
176
     * Get a node from the container.
177
     *
178
     * @param string $path
179
     *
180
     * @return DataContainerInterface
181
     */
182 4
    public function node(string $path): DataContainerInterface
183
    {
184 4
        $data = $this->get($path, []);
185 4
        return new static(
186 4
            is_array($data)
187 3
                ? $data
188 4
                : []
189
        );
190
    }
191
192
    /**
193
     * Copy paths matching a pattern to another path.
194
     *
195
     * @param string $pattern
196
     * @param string $replacement
197
     *
198
     * @return void
199
     */
200 4
    public function copy(string $pattern, string $replacement)
201
    {
202 4
        $expanded = $this->expand($pattern, $replacement);
203 4
        foreach ($expanded as $source => $destination) {
204 4
            $this->set($destination, $this->get($source));
205
        }
206 4
    }
207
208
    /**
209
     * Move paths matching a pattern to another path.
210
     *
211
     * @param string $pattern
212
     * @param string $replacement
213
     *
214
     * @return void
215
     */
216 4
    public function move(string $pattern, string $replacement)
217
    {
218 4
        $expanded = $this->expand($pattern, $replacement);
219 4
        foreach ($expanded as $source => $destination) {
220 4
            if ($source !== $destination) {
221 4
                $this->set($destination, $this->get($source));
222 4
                if (strpos($destination, $source . static::SEPARATOR) !== 0) {
223 3
                    $this->remove($source);
224
                }
225
            }
226
        }
227 4
    }
228
229
    /**
230
     * Parse a path into an array.
231
     *
232
     * @param string $path
233
     *
234
     * @return array
235
     */
236 12
    private function parsePath(string $path): array
237
    {
238 12
        return array_map(
239
            function (string $key) {
240 12
                return ctype_digit($key)
241 3
                    ? intval($key)
242 12
                    : $key;
243 12
            },
244 12
            array_filter(explode(static::SEPARATOR, $path), 'strlen')
245
        );
246
    }
247
248
    /**
249
     * Get reference to a data node, create it if it does not exist.
250
     *
251
     * @param array $keys
252
     *
253
     * @return array
254
     */
255 7
    private function &getNodeReference(array $keys): array
256
    {
257 7
        $current =& $this->data;
258
259 7
        while (count($keys)) {
260 5
            $key = array_shift($keys);
261 5
            if (!array_key_exists($key, $current)
262 5
                || !is_array($current[$key])
263
            ) {
264 1
                $current[$key] = [];
265
            }
266
267 5
            $current =& $current[$key];
268
        }
269
270 7
        return $current;
271
    }
272
273
    /**
274
     * Find paths in an array by an array of patterns.
275
     *
276
     * @param array    $data
277
     * @param string[] $patterns
278
     * @param string   $prefix
279
     *
280
     * @return array
281
     */
282 7
    private function findArrayPathsByPatterns(
283
        array $data,
284
        array $patterns,
285
        string $prefix
286
    ): array {
287 7
        $pattern      = array_shift($patterns);
288 7
        $matchingKeys = array_filter(
289 7
            array_keys($data),
290
            function ($key) use ($pattern) {
291 7
                return fnmatch($pattern, $key, FNM_NOESCAPE);
292 7
            }
293
        );
294
295 7
        $paths = [];
296 7
        foreach ($matchingKeys as $key) {
297 7
            $path = $prefix . $key;
298
299 7
            if (count($patterns) === 0) {
300 7
                $paths[] = $path;
301 7
                continue;
302
            }
303
304 5
            if (is_array($data[$key])) {
305 5
                $paths = array_merge(
306 5
                    $paths,
307 5
                    $this->findArrayPathsByPatterns(
308 5
                        $data[$key],
309 5
                        $patterns,
310 5
                        $path . static::SEPARATOR
311
                    )
312
                );
313
            }
314
        }
315
316 7
        return $paths;
317
    }
318
319
    /**
320
     * Retrieve an external iterator.
321
     *
322
     * @return Traversable
323
     */
324 1
    public function getIterator(): Traversable
325
    {
326 1
        return new ArrayIterator($this->all());
327
    }
328
}
329