Passed
Push — master ( 1a17c3...faad8c )
by
unknown
01:09
created

DataContainer::move()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 4
nop 2
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 4
rs 9.2
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
/**
10
 * Contains any data which can be accessed using dot-notation.
11
 */
12
class DataContainer implements DataContainerInterface
13
{
14
    /** @var array */
15
    private $data;
16
17
    /**
18
     * Constructor.
19
     *
20
     * @param array $data
21
     */
22 1
    public function __construct(array $data = [])
23
    {
24 1
        $this->data = $data;
25 1
    }
26
27
    /**
28
     * Check whether a path exists.
29
     *
30
     * @param string $path
31
     *
32
     * @return bool
33
     */
34 6
    public function has(string $path): bool
35
    {
36 6
        $random = md5(uniqid());
37 6
        return $this->get($path, $random) !== $random;
38
    }
39
40
    /**
41
     * Get a value of a path.
42
     *
43
     * @param string $path
44
     * @param mixed  $default
45
     *
46
     * @return mixed
47
     */
48 6
    public function get(string $path, $default = null)
49
    {
50 6
        return array_reduce(
51 6
            $this->parsePath($path),
52 6
            function ($data, $key) use ($default) {
53 6
                return is_array($data) && array_key_exists($key, $data)
54 4
                    ? $data[$key]
55 6
                    : $default;
56 6
            },
57 6
            $this->data
58
        );
59
    }
60
61
    /**
62
     * Get the contained array.
63
     *
64
     * @return array
65
     */
66 1
    public function all(): array
67
    {
68 1
        return $this->data;
69
    }
70
71
    /**
72
     * Set a value on a path.
73
     *
74
     * @param string $path
75
     * @param mixed  $value
76
     *
77
     * @return void
78
     */
79 4 View Code Duplication
    public function set(string $path, $value = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
    {
81 4
        $keys        = $this->parsePath($path);
82 4
        $last        = array_pop($keys);
83 4
        $node        =& $this->getNodeReference($keys);
84 4
        $node[$last] = $value;
85 4
    }
86
87
    /**
88
     * Remove a path if it exists.
89
     *
90
     * @param string $pattern
91
     *
92
     * @return void
93
     */
94 4 View Code Duplication
    public function remove(string $pattern)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
95
    {
96 4
        foreach ($this->glob($pattern) as $path) {
97 3
            $keys = $this->parsePath($path);
98 3
            $last = array_pop($keys);
99 3
            $node =& $this->getNodeReference($keys);
100 3
            unset($node[$last]);
101
        }
102 4
    }
103
104
    /**
105
     * Find paths that match a pattern.
106
     *
107
     * @param string $pattern
108
     *
109
     * @return string[]
110
     */
111 6
    public function glob(string $pattern): array
112
    {
113 6
        return $this->findArrayPathsByPatterns(
114 6
            $this->data,
115 6
            explode(static::SEPARATOR, $pattern),
116 6
            ''
117
        );
118
    }
119
120
    /**
121
     * Find paths that match a pattern an their replacements.
122
     *
123
     * @param string $pattern
124
     * @param string $replacement
125
     *
126
     * @return string[]
127
     */
128 5
    public function expand(string $pattern, string $replacement): array
129
    {
130 5
        $matches = $this->glob($pattern);
131 5
        $regex   = $this->getGlobRegex($pattern);
132 5
        return array_combine(
133 5
            $matches,
134 5
            array_map(
135 5
                function ($match) use ($regex, $replacement) {
136 5
                    return $this->replaceByRegex($regex, $match, $replacement);
137 5
                },
138 5
                $matches
139
            )
140
        );
141
    }
142
143
    /**
144
     * Branch into a list of data containers.
145
     *
146
     * @param string $pattern
147
     *
148
     * @return DataContainerInterface[]
149
     */
150 4
    public function branch(string $pattern): array
151
    {
152 4
        return array_map(
153 4
            function (array $data) : DataContainerInterface {
154 4
                return new static($data);
155 4
            },
156 4
            array_map(
157 4
                function (string $path) : array {
158 4
                    return (array) $this->get($path, []);
159 4
                },
160 4
                $this->glob($pattern)
161
            )
162
        );
163
    }
164
165
    /**
166
     * Get a node from the container.
167
     *
168
     * @param string $path
169
     *
170
     * @return DataContainerInterface
171
     */
172 4
    public function node(string $path): DataContainerInterface
173
    {
174 4
        $data = $this->get($path, []);
175 4
        return new static(
176 4
            is_array($data)
177 3
                ? $data
178 4
                : []
179
        );
180
    }
181
182
    /**
183
     * Copy paths matching a pattern to another path.
184
     *
185
     * @param string $pattern
186
     * @param string $replacement
187
     *
188
     * @return void
189
     */
190 4
    public function copy(string $pattern, string $replacement)
191
    {
192 4
        $expanded = $this->expand($pattern, $replacement);
193 4
        foreach ($expanded as $source => $destination) {
194 4
            $this->set($destination, $this->get($source));
195
        }
196 4
    }
197
198
    /**
199
     * Move paths matching a pattern to another path.
200
     *
201
     * @param string $pattern
202
     * @param string $replacement
203
     *
204
     * @return void
205
     */
206 4
    public function move(string $pattern, string $replacement)
207
    {
208 4
        $expanded = $this->expand($pattern, $replacement);
209 4
        foreach ($expanded as $source => $destination) {
210 4
            if ($source !== $destination) {
211 4
                $this->set($destination, $this->get($source));
212 4
                if (strpos($destination, $source . static::SEPARATOR) !== 0) {
213 4
                    $this->remove($source);
214
                }
215
            }
216
        }
217 4
    }
218
219
    /**
220
     * Parse a path into an array.
221
     *
222
     * @param string $path
223
     *
224
     * @return array
225
     */
226 12
    private function parsePath(string $path): array
227
    {
228 12
        return array_map(
229 12
            function (string $key) {
230 12
                return ctype_digit($key)
231 3
                    ? intval($key)
232 12
                    : $key;
233 12
            },
234 12
            array_filter(explode(static::SEPARATOR, $path), 'strlen')
235
        );
236
    }
237
238
    /**
239
     * Get reference to a data node, create it if it does not exist.
240
     *
241
     * @param array $keys
242
     *
243
     * @return array
244
     */
245 7
    private function &getNodeReference(array $keys): array
246
    {
247 7
        $current =& $this->data;
248
249 7
        while (count($keys)) {
250 5
            $key = array_shift($keys);
251 5
            if (!array_key_exists($key, $current)
252 5
                || !is_array($current[$key])
253
            ) {
254 1
                $current[$key] = [];
255
            }
256
257 5
            $current =& $current[$key];
258
        }
259
260 7
        return $current;
261
    }
262
263
    /**
264
     * Find paths in an array by an array of patterns.
265
     *
266
     * @param array    $data
267
     * @param string[] $patterns
268
     * @param string   $prefix
269
     *
270
     * @return array
271
     */
272 6
    private function findArrayPathsByPatterns(
273
        array $data,
274
        array $patterns,
275
        string $prefix
276
    ): array {
277 6
        $pattern      = array_shift($patterns);
278 6
        $matchingKeys = array_filter(
279 6
            array_keys($data),
280 6
            function ($key) use ($pattern) {
281 6
                return fnmatch($pattern, $key);
282 6
            }
283
        );
284
285 6
        $paths = [];
286 6
        foreach ($matchingKeys as $key) {
287 6
            $path = $prefix . $key;
288
289 6
            if (count($patterns) === 0) {
290 6
                $paths[] = $path;
291 6
                continue;
292
            }
293
294 4
            if (is_array($data[$key])) {
295 4
                $paths = array_merge(
296 4
                    $paths,
297 4
                    $this->findArrayPathsByPatterns(
298 4
                        $data[$key],
299 4
                        $patterns,
300 4
                        $path . static::SEPARATOR
301
                    )
302
                );
303
            }
304
        }
305
306 6
        return $paths;
307
    }
308
309
    /**
310
     * Get a replacement for pattern that has been matched by glob.
311
     *
312
     * @param string $regex
313
     * @param string $match
314
     * @param string $replacement
315
     *
316
     * @return string
317
     */
318 5
    private function replaceByRegex(
319
        string $regex,
320
        string $match,
321
        string $replacement
322
    ): string {
323 5
        if (preg_match($regex, $match, $matches)) {
324 5
            $replacement = preg_replace_callback(
325 5
                '/\$([\d]+)/',
326 5
                function (array $match) use ($matches) {
327 4
                    return array_key_exists($match[1], $matches)
328 4
                        ? $matches[$match[1]]
329 4
                        : $match[0];
330 5
                },
331 5
                $replacement
332
            );
333
        }
334
335 5
        return $replacement;
336
    }
337
338
    /**
339
     * Get regex pattern for a glob pattern.
340
     *
341
     * @param string $pattern
342
     *
343
     * @return string
344
     */
345 5
    private function getGlobRegex(
346
        string $pattern
347
    ): string {
348
        $transforms = [
349 5
            '\*'   => '([^' . preg_quote(static::SEPARATOR, '#') . ']*)',
350 5
            '\?'   => '(.)',
351 5
            '\[\!' => '([^',
352 5
            '\['   => '([',
353 5
            '\]'   => '])'
354
        ];
355
356 5
        return sprintf(
357 5
            '#^%s$#',
358 5
            strtr(preg_quote($pattern, '#'), $transforms)
359
        );
360
    }
361
}
362