Test Failed
Push — master ( faad8c...154f3e )
by Rieks
03:16
created

DataContainer::findArrayPathsByPatterns()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 19
nc 4
nop 3
dl 0
loc 35
rs 8.5806
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
    public function __construct(array $data = [])
23
    {
24
        $this->data = $data;
25
    }
26
27
    /**
28
     * Check whether a path exists.
29
     *
30
     * @param string $path
31
     *
32
     * @return bool
33
     */
34
    public function has(string $path): bool
35
    {
36
        $random = md5(uniqid());
37
        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
    public function get(string $path, $default = null)
49
    {
50
        return array_reduce(
51
            $this->parsePath($path),
52
            function ($data, $key) use ($default) {
53
                return is_array($data) && array_key_exists($key, $data)
54
                    ? $data[$key]
55
                    : $default;
56
            },
57
            $this->data
58
        );
59
    }
60
61
    /**
62
     * Get the contained array.
63
     *
64
     * @return array
65
     */
66
    public function all(): array
67
    {
68
        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 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
        $keys        = $this->parsePath($path);
82
        $last        = array_pop($keys);
83
        $node        =& $this->getNodeReference($keys);
84
        $node[$last] = $value;
85
    }
86
87
    /**
88
     * Remove a path if it exists.
89
     *
90
     * @param string $pattern
91
     *
92
     * @return void
93
     */
94 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
        foreach ($this->glob($pattern) as $path) {
97
            $keys = $this->parsePath($path);
98
            $last = array_pop($keys);
99
            $node =& $this->getNodeReference($keys);
100
            unset($node[$last]);
101
        }
102
    }
103
104
    /**
105
     * Find paths that match a pattern.
106
     *
107
     * @param string $pattern
108
     *
109
     * @return string[]
110
     */
111
    public function glob(string $pattern): array
112
    {
113
        return $this->findArrayPathsByPatterns(
114
            $this->data,
115
            explode(static::SEPARATOR, $pattern),
116
            ''
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
    public function expand(string $pattern, string $replacement): array
129
    {
130
        $matches = $this->glob($pattern);
131
        $regex   = $this->getGlobRegex($pattern);
132
        return array_combine(
133
            $matches,
134
            array_map(
135
                function ($match) use ($regex, $replacement) {
136
                    return $this->replaceByRegex($regex, $match, $replacement);
137
                },
138
                $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
    public function branch(string $pattern): array
151
    {
152
        return array_map(
153
            function (array $data) : DataContainerInterface {
154
                return new static($data);
155
            },
156
            array_map(
157
                function (string $path) : array {
158
                    return (array) $this->get($path, []);
159
                },
160
                $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
    public function node(string $path): DataContainerInterface
173
    {
174
        $data = $this->get($path, []);
175
        return new static(
176
            is_array($data)
177
                ? $data
178
                : []
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
    public function copy(string $pattern, string $replacement)
191
    {
192
        $expanded = $this->expand($pattern, $replacement);
193
        foreach ($expanded as $source => $destination) {
194
            $this->set($destination, $this->get($source));
195
        }
196
    }
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
    public function move(string $pattern, string $replacement)
207
    {
208
        $expanded = $this->expand($pattern, $replacement);
209
        foreach ($expanded as $source => $destination) {
210
            if ($source !== $destination) {
211
                $this->set($destination, $this->get($source));
212
                if (strpos($destination, $source . static::SEPARATOR) !== 0) {
213
                    $this->remove($source);
214
                }
215
            }
216
        }
217
    }
218
219
    /**
220
     * Parse a path into an array.
221
     *
222
     * @param string $path
223
     *
224
     * @return array
225
     */
226
    private function parsePath(string $path): array
227
    {
228
        return array_map(
229
            function (string $key) {
230
                return ctype_digit($key)
231
                    ? intval($key)
232
                    : $key;
233
            },
234
            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
    private function &getNodeReference(array $keys): array
246
    {
247
        $current =& $this->data;
248
249
        while (count($keys)) {
250
            $key = array_shift($keys);
251
            if (!array_key_exists($key, $current)
252
                || !is_array($current[$key])
253
            ) {
254
                $current[$key] = [];
255
            }
256
257
            $current =& $current[$key];
258
        }
259
260
        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
    private function findArrayPathsByPatterns(
273
        array $data,
274
        array $patterns,
275
        string $prefix
276
    ): array {
277
        $pattern      = array_shift($patterns);
278
        $matchingKeys = array_filter(
279
            array_keys($data),
280
            function ($key) use ($pattern) {
281
                return fnmatch($pattern, $key);
282
            }
283
        );
284
285
        $paths = [];
286
        foreach ($matchingKeys as $key) {
287
            $path = $prefix . $key;
288
289
            if (count($patterns) === 0) {
290
                $paths[] = $path;
291
                continue;
292
            }
293
294
            if (is_array($data[$key])) {
295
                $paths = array_merge(
296
                    $paths,
297
                    $this->findArrayPathsByPatterns(
298
                        $data[$key],
299
                        $patterns,
300
                        $path . static::SEPARATOR
301
                    )
302
                );
303
            }
304
        }
305
306
        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
    private function replaceByRegex(
319
        string $regex,
320
        string $match,
321
        string $replacement
322
    ): string {
323
        if (preg_match($regex, $match, $matches)) {
324
            $replacement = preg_replace_callback(
325
                '/\$([\d]+)/',
326
                function (array $match) use ($matches) {
327
                    return array_key_exists($match[1], $matches)
328
                        ? $matches[$match[1]]
329
                        : $match[0];
330
                },
331
                $replacement
332
            );
333
        }
334
335
        return $replacement;
336
    }
337
338
    /**
339
     * Get regex pattern for a glob pattern.
340
     *
341
     * @param string $pattern
342
     *
343
     * @return string
344
     */
345
    private function getGlobRegex(
346
        string $pattern
347
    ): string {
348
        $transforms = [
349
            '\*'   => '([^' . preg_quote(static::SEPARATOR, '#') . ']*)',
350
            '\?'   => '(.)',
351
            '\[\!' => '([^',
352
            '\['   => '([',
353
            '\]'   => '])'
354
        ];
355
356
        return sprintf(
357
            '#^%s$#',
358
            strtr(preg_quote($pattern, '#'), $transforms)
359
        );
360
    }
361
}
362