Passed
Pull Request — master (#8)
by Jan-Marten
07:01
created

DataContainer::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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