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) |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|
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.