Passed
Pull Request — master (#13)
by Ashoka
04:51
created

DataContainer::enclose()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 14
ccs 11
cts 11
cp 1
rs 9.9332
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
 * Copyright MediaCT. All rights reserved.
5
 * https://www.mediact.nl
6
 */
7
8
namespace Mediact\DataContainer;
9
10
use ArrayIterator;
11
use Traversable;
12
13
/**
14
 * Contains any data which can be accessed using dot-notation.
15
 */
16
class DataContainer implements IterableDataContainerInterface
0 ignored issues
show
Deprecated Code introduced by
The interface Mediact\DataContainer\It...eDataContainerInterface has been deprecated: DataContainerInterface is iterable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

16
class DataContainer implements /** @scrutinizer ignore-deprecated */ IterableDataContainerInterface

This interface has been deprecated. The supplier of the interface has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.

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