1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the puli/repository package. |
5
|
|
|
* |
6
|
|
|
* (c) Bernhard Schussek <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Puli\Repository; |
13
|
|
|
|
14
|
|
|
use InvalidArgumentException; |
15
|
|
|
use Puli\Repository\Api\ChangeStream\VersionList; |
16
|
|
|
use Puli\Repository\Api\EditableRepository; |
17
|
|
|
use Puli\Repository\Api\NoVersionFoundException; |
18
|
|
|
use Puli\Repository\Api\Resource\PuliResource; |
19
|
|
|
use RecursiveIteratorIterator; |
20
|
|
|
use Webmozart\Glob\Glob; |
21
|
|
|
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator; |
22
|
|
|
use Webmozart\PathUtil\Path; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* A repository backed by a JSON file optimized for development. |
26
|
|
|
* |
27
|
|
|
* The generated JSON file is described by res/schema/repository-schema-1.0.json. |
28
|
|
|
* |
29
|
|
|
* Resources can be added with the method {@link add()}: |
30
|
|
|
* |
31
|
|
|
* ```php |
32
|
|
|
* use Puli\Repository\JsonRepository; |
33
|
|
|
* |
34
|
|
|
* $repo = new JsonRepository('/path/to/repository.json', '/path/to/project'); |
35
|
|
|
* $repo->add('/css', new DirectoryResource('/path/to/project/res/css')); |
36
|
|
|
* ``` |
37
|
|
|
* |
38
|
|
|
* When adding a resource, the added filesystem path is stored in the JSON file |
39
|
|
|
* under the key of the Puli path. The path is stored relatively to the base |
40
|
|
|
* directory passed to the constructor: |
41
|
|
|
* |
42
|
|
|
* ```json |
43
|
|
|
* { |
44
|
|
|
* "/css": "res/css" |
45
|
|
|
* } |
46
|
|
|
* ``` |
47
|
|
|
* |
48
|
|
|
* Mapped resources can be read with the method {@link get()}: |
49
|
|
|
* |
50
|
|
|
* ```php |
51
|
|
|
* $cssPath = $repo->get('/css')->getFilesystemPath(); |
52
|
|
|
* ``` |
53
|
|
|
* |
54
|
|
|
* You can also access nested files: |
55
|
|
|
* |
56
|
|
|
* ```php |
57
|
|
|
* echo $repo->get('/css/style.css')->getBody(); |
58
|
|
|
* ``` |
59
|
|
|
* |
60
|
|
|
* Nested files are searched during {@link get()}. As a consequence, this |
61
|
|
|
* implementation should not be used in production environments. Use |
62
|
|
|
* {@link OptimizedJsonRepository} instead which searches nested files during |
63
|
|
|
* {@link add()} and has much faster read performance. |
64
|
|
|
* |
65
|
|
|
* @since 1.0 |
66
|
|
|
* |
67
|
|
|
* @author Bernhard Schussek <[email protected]> |
68
|
|
|
* @author Titouan Galopin <[email protected]> |
69
|
|
|
*/ |
70
|
|
|
class JsonRepository extends AbstractJsonRepository implements EditableRepository |
71
|
|
|
{ |
72
|
|
|
/** |
73
|
|
|
* Flag: Don't search the contents of mapped directories for matching paths. |
74
|
|
|
* |
75
|
|
|
* @internal |
76
|
|
|
*/ |
77
|
|
|
const NO_SEARCH_FILESYSTEM = 2; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Flag: Don't filter out references that don't exist on the filesystem. |
81
|
|
|
* |
82
|
|
|
* @internal |
83
|
|
|
*/ |
84
|
|
|
const NO_CHECK_FILE_EXISTS = 4; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Flag: Include the references for mapped ancestor paths /a of a path /a/b. |
88
|
|
|
* |
89
|
|
|
* @internal |
90
|
|
|
*/ |
91
|
|
|
const INCLUDE_ANCESTORS = 8; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Flag: Include the references for mapped nested paths /a/b of a path /a. |
95
|
|
|
* |
96
|
|
|
* @internal |
97
|
|
|
*/ |
98
|
|
|
const INCLUDE_NESTED = 16; |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Creates a new repository. |
102
|
|
|
* |
103
|
|
|
* @param string $path The path to the JSON file. If relative, it |
104
|
|
|
* must be relative to the base directory. |
105
|
|
|
* @param string $baseDirectory The base directory of the store. Paths |
106
|
|
|
* inside that directory are stored as relative |
107
|
|
|
* paths. Paths outside that directory are |
108
|
|
|
* stored as absolute paths. |
109
|
|
|
* @param bool $validateJson Whether to validate the JSON file against |
110
|
|
|
* the schema. Slow but spots problems. |
111
|
|
|
*/ |
112
|
198 |
|
public function __construct($path, $baseDirectory, $validateJson = false) |
113
|
|
|
{ |
114
|
|
|
// Does not accept ChangeStream objects |
115
|
|
|
// The ChangeStream functionality is implemented by the repository itself |
116
|
198 |
|
parent::__construct($path, $baseDirectory, $validateJson); |
117
|
198 |
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* {@inheritdoc} |
121
|
|
|
*/ |
122
|
32 |
|
public function getVersions($path) |
123
|
|
|
{ |
124
|
32 |
|
if (!$this->json) { |
|
|
|
|
125
|
13 |
|
$this->load(); |
126
|
|
|
} |
127
|
|
|
|
128
|
32 |
|
$references = $this->searchReferences($path); |
129
|
|
|
|
130
|
32 |
|
if (!isset($references[$path])) { |
131
|
10 |
|
throw NoVersionFoundException::forPath($path); |
132
|
|
|
} |
133
|
|
|
|
134
|
22 |
|
$resources = array(); |
135
|
22 |
|
$pathReferences = $references[$path]; |
136
|
|
|
|
137
|
|
|
// The first reference is the last (current) version |
138
|
|
|
// Hence traverse in reverse order |
139
|
22 |
|
for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) { |
140
|
22 |
|
$resources[] = $this->createResource($path, $ref); |
141
|
|
|
} |
142
|
|
|
|
143
|
22 |
|
return new VersionList($path, $resources); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* {@inheritdoc} |
148
|
|
|
*/ |
149
|
170 |
|
protected function storeVersion(PuliResource $resource) |
150
|
|
|
{ |
151
|
170 |
|
$path = $resource->getPath(); |
152
|
|
|
|
153
|
|
|
// Newly inserted parent directories and the resource need to be |
154
|
|
|
// sorted before we can correctly search references below |
155
|
170 |
|
krsort($this->json); |
156
|
|
|
|
157
|
|
|
// If a mapping exists for a sub-path of this resource |
158
|
|
|
// (e.g. $path = /a, mapped sub-path = /a/b) |
159
|
|
|
// we need to record the order, since by default sub-paths are |
160
|
|
|
// preferred over super paths |
161
|
|
|
|
162
|
170 |
|
$references = $this->searchReferences( |
163
|
|
|
$path, |
164
|
|
|
// Don't do filesystem checks here. We only check the filesystem |
165
|
|
|
// when reading, not when adding. |
166
|
170 |
|
self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS |
167
|
|
|
// Include references for mapped ancestor and nested paths |
168
|
170 |
|
| self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED |
169
|
|
|
); |
170
|
|
|
|
171
|
|
|
// Filter virtual resources |
172
|
170 |
|
$references = array_filter($references, function ($currentReferences) { |
173
|
170 |
|
return array(null) !== $currentReferences; |
174
|
170 |
|
}); |
175
|
|
|
|
176
|
|
|
// The $references contain: |
177
|
|
|
// - any sub references (e.g. /a/b/c, /a/b/d) |
178
|
|
|
// - the reference itself at $pos (e.g. /a/b) |
179
|
|
|
// - non-null parent references (e.g. /a) |
180
|
|
|
// (in that order, since long paths are sorted before short paths) |
181
|
170 |
|
$pos = array_search($path, array_keys($references), true); |
182
|
|
|
|
183
|
|
|
// We need to do three things: |
184
|
|
|
|
185
|
|
|
// 1. If any parent mapping has an order defined, inherit that order |
186
|
|
|
|
187
|
170 |
|
if ($pos + 1 < count($references)) { |
188
|
|
|
// Inherit the parent order if necessary |
189
|
14 |
|
if (!isset($this->json['_order'][$path])) { |
190
|
14 |
|
$parentReferences = array_slice($references, $pos + 1); |
191
|
|
|
|
192
|
14 |
|
$this->initWithParentOrder($path, $parentReferences); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// A parent order was inherited. Insert the path itself. |
196
|
14 |
|
if (isset($this->json['_order'][$path])) { |
197
|
2 |
|
$this->prependOrderEntry($path, $path); |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
// 2. If there are child mappings, insert the current path into their order |
202
|
|
|
|
203
|
170 |
|
if ($pos > 0) { |
204
|
10 |
|
$subReferences = array_slice($references, 0, $pos); |
205
|
|
|
|
206
|
10 |
|
foreach ($subReferences as $subPath => $_) { |
207
|
10 |
|
if (isset($this->json['_order'][$subPath])) { |
208
|
6 |
|
continue; |
209
|
|
|
} |
210
|
|
|
|
211
|
10 |
|
if (isset($this->json['_order'][$path])) { |
212
|
|
|
$this->json['_order'][$subPath] = $this->json['_order'][$path]; |
213
|
|
|
} else { |
214
|
10 |
|
$this->initWithDefaultOrder($subPath, $path, $references); |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// After initializing all order entries, insert the new one |
219
|
10 |
|
foreach ($subReferences as $subPath => $_) { |
220
|
10 |
|
$this->prependOrderEntry($subPath, $path); |
221
|
|
|
} |
222
|
|
|
} |
223
|
170 |
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* {@inheritdoc} |
227
|
|
|
*/ |
228
|
170 |
|
protected function insertReference($path, $reference) |
229
|
|
|
{ |
230
|
170 |
|
if (!isset($this->json[$path])) { |
231
|
|
|
// Store first entries as simple reference |
232
|
170 |
|
$this->json[$path] = $reference; |
233
|
|
|
|
234
|
170 |
|
return; |
235
|
|
|
} |
236
|
|
|
|
237
|
14 |
|
if ($reference === $this->json[$path]) { |
238
|
|
|
// Reference is already set |
239
|
4 |
|
return; |
240
|
|
|
} |
241
|
|
|
|
242
|
10 |
|
if (!is_array($this->json[$path])) { |
243
|
|
|
// Convert existing entries to arrays for follow ups |
244
|
10 |
|
$this->json[$path] = array($this->json[$path]); |
245
|
|
|
} |
246
|
|
|
|
247
|
10 |
|
if (!in_array($reference, $this->json[$path], true)) { |
248
|
|
|
// Insert at the beginning of the array |
249
|
10 |
|
array_unshift($this->json[$path], $reference); |
250
|
|
|
} |
251
|
10 |
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* {@inheritdoc} |
255
|
|
|
*/ |
256
|
22 |
|
protected function removeReferences($glob) |
257
|
|
|
{ |
258
|
22 |
|
$checkResults = $this->getReferencesForGlob($glob); |
259
|
22 |
|
$nonDeletablePaths = array(); |
260
|
|
|
|
261
|
22 |
|
foreach ($checkResults as $path => $filesystemPath) { |
262
|
22 |
|
if (!array_key_exists($path, $this->json)) { |
263
|
22 |
|
$nonDeletablePaths[] = $filesystemPath; |
264
|
|
|
} |
265
|
|
|
} |
266
|
|
|
|
267
|
22 |
|
if (count($nonDeletablePaths) > 0) { |
268
|
2 |
|
throw new InvalidArgumentException(sprintf( |
269
|
|
|
'You cannot remove resources that are not mapped in the JSON '. |
270
|
2 |
|
'file. Tried to remove %s%s.', |
271
|
|
|
reset($nonDeletablePaths), |
272
|
2 |
|
count($nonDeletablePaths) > 1 |
273
|
|
|
? ' and '.(count($nonDeletablePaths) - 1).' more' |
274
|
2 |
|
: '' |
275
|
|
|
)); |
276
|
|
|
} |
277
|
|
|
|
278
|
20 |
|
$deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM); |
279
|
20 |
|
$removed = 0; |
280
|
|
|
|
281
|
20 |
|
foreach ($deletedPaths as $path => $filesystemPath) { |
282
|
20 |
|
$removed += 1 + count($this->getReferencesForGlob($path.'/**/*')); |
283
|
|
|
|
284
|
20 |
|
unset($this->json[$path]); |
285
|
|
|
} |
286
|
|
|
|
287
|
20 |
|
return $removed; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* {@inheritdoc} |
292
|
|
|
*/ |
293
|
110 |
|
protected function getReferencesForPath($path) |
294
|
|
|
{ |
295
|
|
|
// Stop on first result and flatten |
296
|
110 |
|
return $this->flatten($this->searchReferences($path, self::STOP_ON_FIRST)); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* {@inheritdoc} |
301
|
|
|
*/ |
302
|
50 |
View Code Duplication |
protected function getReferencesForGlob($glob, $flags = 0) |
|
|
|
|
303
|
|
|
{ |
304
|
50 |
|
if (!Glob::isDynamic($glob)) { |
305
|
34 |
|
return $this->getReferencesForPath($glob); |
306
|
|
|
} |
307
|
|
|
|
308
|
36 |
|
return $this->getReferencesForRegex( |
309
|
36 |
|
Glob::getBasePath($glob), |
310
|
36 |
|
Glob::toRegEx($glob), |
311
|
|
|
$flags |
312
|
|
|
); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* {@inheritdoc} |
317
|
|
|
*/ |
318
|
68 |
|
protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0) |
319
|
|
|
{ |
320
|
68 |
|
return $this->flattenWithFilter( |
321
|
|
|
// Never stop on the first result before applying the filter since |
322
|
|
|
// the filter may reject the only returned path |
323
|
68 |
|
$this->searchReferences($staticPrefix, self::INCLUDE_NESTED), |
324
|
|
|
$regex, |
325
|
|
|
$flags, |
326
|
|
|
$maxDepth |
327
|
|
|
); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* {@inheritdoc} |
332
|
|
|
*/ |
333
|
26 |
View Code Duplication |
protected function getReferencesInDirectory($path, $flags = 0) |
|
|
|
|
334
|
|
|
{ |
335
|
26 |
|
$basePath = rtrim($path, '/'); |
336
|
|
|
|
337
|
26 |
|
return $this->getReferencesForRegex( |
338
|
26 |
|
$basePath.'/', |
339
|
26 |
|
'~^'.preg_quote($basePath, '~').'/[^/]+$~', |
340
|
|
|
$flags, |
341
|
|
|
// Limit the directory exploration to the depth of the path + 1 |
342
|
26 |
|
$this->getPathDepth($path) + 1 |
343
|
|
|
); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Flattens a two-level reference array into a one-level array. |
348
|
|
|
* |
349
|
|
|
* For each entry on the first level, only the first entry of the second |
350
|
|
|
* level is included in the result. |
351
|
|
|
* |
352
|
|
|
* Each reference returned by this method can be: |
353
|
|
|
* |
354
|
|
|
* * `null` |
355
|
|
|
* * a link starting with `@` |
356
|
|
|
* * an absolute filesystem path |
357
|
|
|
* |
358
|
|
|
* The keys of the returned array are Puli paths. Their order is undefined. |
359
|
|
|
* |
360
|
|
|
* @param array $references A two-level reference array as returned by |
361
|
|
|
* {@link searchReferences()}. |
362
|
|
|
* |
363
|
|
|
* @return string[]|null[] A one-level array of references with Puli paths |
364
|
|
|
* as keys. |
365
|
|
|
*/ |
366
|
110 |
|
private function flatten(array $references) |
367
|
|
|
{ |
368
|
110 |
|
$result = array(); |
369
|
|
|
|
370
|
110 |
|
foreach ($references as $currentPath => $currentReferences) { |
371
|
102 |
|
if (!isset($result[$currentPath])) { |
372
|
102 |
|
$result[$currentPath] = reset($currentReferences); |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
110 |
|
return $result; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Flattens a two-level reference array into a one-level array and filters |
381
|
|
|
* out any references that don't match the given regular expression. |
382
|
|
|
* |
383
|
|
|
* This method takes a two-level reference array as returned by |
384
|
|
|
* {@link searchReferences()}. The references are scanned for Puli paths |
385
|
|
|
* matching the given regular expression. Those matches are returned. |
386
|
|
|
* |
387
|
|
|
* If a matching path refers to more than one reference, the first reference |
388
|
|
|
* is returned in the resulting array. |
389
|
|
|
* |
390
|
|
|
* If `$listDirectories` is set to `true`, all references that contain |
391
|
|
|
* directory paths are traversed recursively and scanned for more paths |
392
|
|
|
* matching the regular expression. This recursive traversal can be limited |
393
|
|
|
* by passing a `$maxDepth` (see {@link getPathDepth()}). |
394
|
|
|
* |
395
|
|
|
* Each reference returned by this method can be: |
396
|
|
|
* |
397
|
|
|
* * `null` |
398
|
|
|
* * a link starting with `@` |
399
|
|
|
* * an absolute filesystem path |
400
|
|
|
* |
401
|
|
|
* The keys of the returned array are Puli paths. Their order is undefined. |
402
|
|
|
* |
403
|
|
|
* @param array $references A two-level reference array as returned by |
404
|
|
|
* {@link searchReferences()}. |
405
|
|
|
* @param string $regex A regular expression used to filter Puli paths. |
406
|
|
|
* @param int $flags A bitwise combination of the flag constants in |
407
|
|
|
* this class. |
408
|
|
|
* @param int $maxDepth The maximum path depth when searching the |
409
|
|
|
* contents of directory references. If 0, the |
410
|
|
|
* depth is unlimited. |
411
|
|
|
* |
412
|
|
|
* @return string[]|null[] A one-level array of references with Puli paths |
413
|
|
|
* as keys. |
414
|
|
|
*/ |
415
|
68 |
|
private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0) |
416
|
|
|
{ |
417
|
68 |
|
$result = array(); |
418
|
|
|
|
419
|
68 |
|
foreach ($references as $currentPath => $currentReferences) { |
420
|
|
|
// Check whether the current entry matches the pattern |
421
|
62 |
View Code Duplication |
if (!isset($result[$currentPath]) && preg_match($regex, $currentPath)) { |
|
|
|
|
422
|
|
|
// If yes, the first stored reference is returned |
423
|
28 |
|
$result[$currentPath] = reset($currentReferences); |
424
|
|
|
|
425
|
28 |
|
if ($flags & self::STOP_ON_FIRST) { |
426
|
2 |
|
return $result; |
427
|
|
|
} |
428
|
|
|
} |
429
|
|
|
|
430
|
62 |
|
if ($flags & self::NO_SEARCH_FILESYSTEM) { |
431
|
20 |
|
continue; |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
// First follow any links before we check which of them is a directory |
435
|
62 |
|
$currentReferences = $this->followLinks($currentReferences); |
436
|
62 |
|
$currentPath = rtrim($currentPath, '/'); |
437
|
|
|
|
438
|
|
|
// Search the nested entries if desired |
439
|
62 |
|
foreach ($currentReferences as $baseFilesystemPath) { |
440
|
|
|
// Ignore null values and file paths |
441
|
62 |
|
if (!is_dir($baseFilesystemPath)) { |
442
|
40 |
|
continue; |
443
|
|
|
} |
444
|
|
|
|
445
|
26 |
|
$iterator = new RecursiveIteratorIterator( |
446
|
26 |
|
new RecursiveDirectoryIterator( |
447
|
|
|
$baseFilesystemPath, |
448
|
26 |
|
RecursiveDirectoryIterator::CURRENT_AS_PATHNAME |
449
|
26 |
|
| RecursiveDirectoryIterator::SKIP_DOTS |
450
|
|
|
), |
451
|
26 |
|
RecursiveIteratorIterator::SELF_FIRST |
452
|
|
|
); |
453
|
|
|
|
454
|
26 |
|
if (0 !== $maxDepth) { |
455
|
14 |
|
$currentDepth = $this->getPathDepth($currentPath); |
456
|
14 |
|
$maxIteratorDepth = $maxDepth - $currentDepth; |
457
|
|
|
|
458
|
14 |
|
if ($maxIteratorDepth < 1) { |
459
|
|
|
continue; |
460
|
|
|
} |
461
|
|
|
|
462
|
14 |
|
$iterator->setMaxDepth($maxIteratorDepth); |
463
|
|
|
} |
464
|
|
|
|
465
|
26 |
|
$basePathLength = strlen($baseFilesystemPath); |
466
|
|
|
|
467
|
26 |
|
foreach ($iterator as $nestedFilesystemPath) { |
468
|
24 |
|
$nestedPath = substr_replace($nestedFilesystemPath, $currentPath, 0, $basePathLength); |
469
|
|
|
|
470
|
24 |
View Code Duplication |
if (!isset($result[$nestedPath]) && preg_match($regex, $nestedPath)) { |
|
|
|
|
471
|
24 |
|
$result[$nestedPath] = $nestedFilesystemPath; |
472
|
|
|
|
473
|
24 |
|
if ($flags & self::STOP_ON_FIRST) { |
474
|
62 |
|
return $result; |
475
|
|
|
} |
476
|
|
|
} |
477
|
|
|
} |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
|
481
|
66 |
|
return $result; |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
/** |
485
|
|
|
* Filters the JSON file for all references relevant to a given search path. |
486
|
|
|
* |
487
|
|
|
* The JSON is scanned starting with the longest mapped Puli path. |
488
|
|
|
* |
489
|
|
|
* If the search path is "/a/b", the result includes: |
490
|
|
|
* |
491
|
|
|
* * The references of the mapped path "/a/b". |
492
|
|
|
* * The references of any mapped super path "/a" with the sub-path "/b" |
493
|
|
|
* appended. |
494
|
|
|
* |
495
|
|
|
* If the argument `$includeNested` is set to `true`, the result |
496
|
|
|
* additionally includes: |
497
|
|
|
* |
498
|
|
|
* * The references of any mapped sub path "/a/b/c". |
499
|
|
|
* |
500
|
|
|
* This is useful if you want to look for the children of "/a/b" or scan |
501
|
|
|
* all descendants for paths matching a given pattern. |
502
|
|
|
* |
503
|
|
|
* The result of this method is an array with two levels: |
504
|
|
|
* |
505
|
|
|
* * The first level has Puli paths as keys. |
506
|
|
|
* * The second level contains all references for that path, where the |
507
|
|
|
* first reference has the highest, the last reference the lowest |
508
|
|
|
* priority. The keys of the second level are integers. There may be |
509
|
|
|
* holes between any two keys. |
510
|
|
|
* |
511
|
|
|
* The references of the second level contain: |
512
|
|
|
* |
513
|
|
|
* * `null` values for virtual resources |
514
|
|
|
* * strings starting with "@" for links |
515
|
|
|
* * absolute filesystem paths for filesystem resources |
516
|
|
|
* |
517
|
|
|
* @param string $searchPath The path to search. |
518
|
|
|
* @param int $flags A bitwise combination of the flag constants in |
519
|
|
|
* this class. |
520
|
|
|
* |
521
|
|
|
* @return array An array with two levels. |
522
|
|
|
*/ |
523
|
174 |
|
private function searchReferences($searchPath, $flags = 0) |
524
|
|
|
{ |
525
|
174 |
|
$result = array(); |
526
|
174 |
|
$foundMatchingMappings = false; |
527
|
174 |
|
$searchPath = rtrim($searchPath, '/'); |
528
|
174 |
|
$searchPathForTest = $searchPath.'/'; |
529
|
|
|
|
530
|
174 |
|
foreach ($this->json as $currentPath => $currentReferences) { |
531
|
174 |
|
$currentPathForTest = rtrim($currentPath, '/').'/'; |
532
|
|
|
|
533
|
|
|
// We found a mapping that matches the search path |
534
|
|
|
// e.g. mapping /a/b for path /a/b |
535
|
174 |
View Code Duplication |
if ($searchPathForTest === $currentPathForTest) { |
|
|
|
|
536
|
174 |
|
$foundMatchingMappings = true; |
537
|
174 |
|
$currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags); |
538
|
|
|
|
539
|
174 |
|
if (empty($currentReferences)) { |
540
|
4 |
|
continue; |
541
|
|
|
} |
542
|
|
|
|
543
|
174 |
|
$result[$currentPath] = $currentReferences; |
544
|
|
|
|
545
|
|
|
// Return unless an explicit mapping order is defined |
546
|
|
|
// In that case, the ancestors need to be searched as well |
547
|
174 |
|
if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) { |
548
|
58 |
|
return $result; |
549
|
|
|
} |
550
|
|
|
|
551
|
174 |
|
continue; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
// We found a mapping that lies within the search path |
555
|
|
|
// e.g. mapping /a/b/c for path /a/b |
556
|
130 |
|
if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) { |
557
|
42 |
|
$foundMatchingMappings = true; |
558
|
42 |
|
$currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags); |
559
|
|
|
|
560
|
42 |
|
if (empty($currentReferences)) { |
561
|
8 |
|
continue; |
562
|
|
|
} |
563
|
|
|
|
564
|
34 |
|
$result[$currentPath] = $currentReferences; |
565
|
|
|
|
566
|
|
|
// Return unless an explicit mapping order is defined |
567
|
|
|
// In that case, the ancestors need to be searched as well |
568
|
34 |
|
if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) { |
569
|
|
|
return $result; |
570
|
|
|
} |
571
|
|
|
|
572
|
34 |
|
continue; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
// We found a mapping that is an ancestor of the search path |
576
|
|
|
// e.g. mapping /a for path /a/b |
577
|
130 |
|
if (0 === strpos($searchPathForTest, $currentPathForTest)) { |
578
|
130 |
|
$foundMatchingMappings = true; |
579
|
|
|
|
580
|
130 |
View Code Duplication |
if ($flags & self::INCLUDE_ANCESTORS) { |
|
|
|
|
581
|
|
|
// Include the references of the ancestor |
582
|
72 |
|
$currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags); |
583
|
|
|
|
584
|
72 |
|
if (empty($currentReferences)) { |
585
|
|
|
continue; |
586
|
|
|
} |
587
|
|
|
|
588
|
72 |
|
$result[$currentPath] = $currentReferences; |
589
|
|
|
|
590
|
|
|
// Return unless an explicit mapping order is defined |
591
|
|
|
// In that case, the ancestors need to be searched as well |
592
|
72 |
|
if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) { |
593
|
|
|
return $result; |
594
|
|
|
} |
595
|
|
|
|
596
|
72 |
|
continue; |
597
|
|
|
} |
598
|
|
|
|
599
|
108 |
|
if ($flags & self::NO_SEARCH_FILESYSTEM) { |
600
|
|
|
continue; |
601
|
|
|
} |
602
|
|
|
|
603
|
|
|
// Check the filesystem directories pointed to by the ancestors |
604
|
|
|
// for the searched path |
605
|
108 |
|
$nestedPath = substr($searchPath, strlen($currentPathForTest)); |
606
|
108 |
|
$currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath; |
607
|
|
|
|
608
|
|
|
// Follow links so that we can check the nested directories in |
609
|
|
|
// the final transitive link targets |
610
|
108 |
|
$currentReferencesResolved = $this->followLinks( |
611
|
|
|
// Never stop on first, since appendNestedPath() might |
612
|
|
|
// discard the first but accept the second entry |
613
|
108 |
|
$this->resolveReferences($currentPath, $currentReferences, $flags & (~self::STOP_ON_FIRST)) |
614
|
|
|
); |
615
|
|
|
|
616
|
|
|
// Append the path and check which of the resulting paths exist |
617
|
108 |
|
$nestedReferences = $this->appendPathAndFilterExisting( |
618
|
|
|
$currentReferencesResolved, |
619
|
|
|
$nestedPath, |
620
|
|
|
$flags |
621
|
|
|
); |
622
|
|
|
|
623
|
|
|
// None of the results exists |
624
|
108 |
|
if (empty($nestedReferences)) { |
625
|
62 |
|
continue; |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
// Return unless an explicit mapping order is defined |
629
|
|
|
// In that case, the ancestors need to be searched as well |
630
|
64 |
|
if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) { |
631
|
|
|
// The nested references already have size 1 |
632
|
50 |
|
return array($currentPathWithNested => $nestedReferences); |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
// We are traversing long keys before short keys |
636
|
|
|
// It could be that this entry already exists. |
637
|
34 |
|
if (!isset($result[$currentPathWithNested])) { |
638
|
22 |
|
$result[$currentPathWithNested] = $nestedReferences; |
639
|
|
|
|
640
|
22 |
|
continue; |
641
|
|
|
} |
642
|
|
|
|
643
|
|
|
// If no explicit mapping order is defined, simply append the |
644
|
|
|
// new references to the existing ones |
645
|
12 |
|
if (!isset($this->json['_order'][$currentPathWithNested])) { |
646
|
2 |
|
$result[$currentPathWithNested] = array_merge( |
647
|
2 |
|
$result[$currentPathWithNested], |
648
|
|
|
$nestedReferences |
649
|
|
|
); |
650
|
|
|
|
651
|
2 |
|
continue; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
// If an explicit mapping order is defined, store the paths |
655
|
|
|
// of the mappings that generated each reference set and |
656
|
|
|
// resolve the order later on |
657
|
10 |
|
if (!isset($result[$currentPathWithNested][$currentPathWithNested])) { |
658
|
10 |
|
$result[$currentPathWithNested] = array( |
659
|
10 |
|
$currentPathWithNested => $result[$currentPathWithNested], |
660
|
|
|
); |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
// Add the new references generated by the current mapping |
664
|
10 |
|
$result[$currentPathWithNested][$currentPath] = $nestedReferences; |
665
|
|
|
|
666
|
10 |
|
continue; |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
// We did not find anything but previously found mappings |
670
|
|
|
// The mappings are sorted alphabetically, so we can safely abort |
671
|
40 |
|
if ($foundMatchingMappings) { |
672
|
40 |
|
break; |
673
|
|
|
} |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
// Resolve the order where it is explicitly set |
677
|
174 |
|
if (!isset($this->json['_order'])) { |
678
|
174 |
|
return $result; |
679
|
|
|
} |
680
|
|
|
|
681
|
10 |
|
foreach ($result as $currentPath => $referencesByMappedPath) { |
682
|
|
|
// If no order is defined for the path or if only one mapped path |
683
|
|
|
// generated references, there's nothing to do |
684
|
10 |
|
if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) { |
685
|
6 |
|
continue; |
686
|
|
|
} |
687
|
|
|
|
688
|
10 |
|
$orderedReferences = array(); |
689
|
|
|
|
690
|
10 |
|
foreach ($this->json['_order'][$currentPath] as $orderEntry) { |
691
|
10 |
|
if (!isset($referencesByMappedPath[$orderEntry['path']])) { |
692
|
|
|
continue; |
693
|
|
|
} |
694
|
|
|
|
695
|
10 |
|
for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) { |
696
|
10 |
|
$orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]); |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
// Only include references of the first mapped path |
700
|
|
|
// Since $stopOnFirst is set, those references have a |
701
|
|
|
// maximum size of 1 |
702
|
10 |
|
if ($flags & self::STOP_ON_FIRST) { |
703
|
10 |
|
break; |
704
|
|
|
} |
705
|
|
|
} |
706
|
|
|
|
707
|
10 |
|
$result[$currentPath] = $orderedReferences; |
708
|
|
|
} |
709
|
|
|
|
710
|
10 |
|
return $result; |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
/** |
714
|
|
|
* Follows any link in a list of references. |
715
|
|
|
* |
716
|
|
|
* This method takes all the given references, checks for links starting |
717
|
|
|
* with "@" and recursively expands those links to their target references. |
718
|
|
|
* The target references may be `null` or absolute filesystem paths. |
719
|
|
|
* |
720
|
|
|
* Null values are returned unchanged. |
721
|
|
|
* |
722
|
|
|
* Absolute filesystem paths are returned unchanged. |
723
|
|
|
* |
724
|
|
|
* @param string[]|null[] $references The references. |
725
|
|
|
* @param int $flags A bitwise combination of the flag |
726
|
|
|
* constants in this class. |
727
|
|
|
* |
728
|
|
|
* @return string[]|null[] The references with all links replaced by their |
729
|
|
|
* target references. If any link pointed to more |
730
|
|
|
* than one target reference, the returned array |
731
|
|
|
* is larger than the passed array (unless the |
732
|
|
|
* argument `$stopOnFirst` was set to `true`). |
733
|
|
|
*/ |
734
|
118 |
|
private function followLinks(array $references, $flags = 0) |
735
|
|
|
{ |
736
|
118 |
|
$result = array(); |
737
|
|
|
|
738
|
118 |
|
foreach ($references as $key => $reference) { |
739
|
|
|
// Not a link |
740
|
118 |
|
if (!$this->isLinkReference($reference)) { |
741
|
118 |
|
$result[] = $reference; |
742
|
|
|
|
743
|
118 |
|
if ($flags & self::STOP_ON_FIRST) { |
744
|
|
|
return $result; |
745
|
|
|
} |
746
|
|
|
|
747
|
118 |
|
continue; |
748
|
|
|
} |
749
|
|
|
|
750
|
|
|
$referencedPath = substr($reference, 1); |
751
|
|
|
|
752
|
|
|
// Get all the file system paths that this link points to |
753
|
|
|
// and append them to the result |
754
|
|
|
foreach ($this->searchReferences($referencedPath, $flags) as $referencedReferences) { |
755
|
|
|
// Follow links recursively |
756
|
|
|
$referencedReferences = $this->followLinks($referencedReferences); |
757
|
|
|
|
758
|
|
|
// Append all resulting target paths to the result |
759
|
|
|
foreach ($referencedReferences as $referencedReference) { |
760
|
|
|
$result[] = $referencedReference; |
761
|
|
|
|
762
|
|
|
if ($flags & self::STOP_ON_FIRST) { |
763
|
|
|
return $result; |
764
|
|
|
} |
765
|
|
|
} |
766
|
|
|
} |
767
|
|
|
} |
768
|
|
|
|
769
|
118 |
|
return $result; |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
/** |
773
|
|
|
* Appends nested paths to references and filters out the existing ones. |
774
|
|
|
* |
775
|
|
|
* This method takes all the given references, appends the nested path to |
776
|
|
|
* each of them and then filters out the results that actually exist on the |
777
|
|
|
* filesystem. |
778
|
|
|
* |
779
|
|
|
* Null references are filtered out. |
780
|
|
|
* |
781
|
|
|
* Link references should be followed with {@link followLinks()} before |
782
|
|
|
* calling this method. |
783
|
|
|
* |
784
|
|
|
* @param string[]|null[] $references The references. |
785
|
|
|
* @param string $nestedPath The nested path to append without |
786
|
|
|
* leading slash ("/"). |
787
|
|
|
* @param int $flags A bitwise combination of the flag |
788
|
|
|
* constants in this class. |
789
|
|
|
* |
790
|
|
|
* @return string[] The references with the nested path appended. Each |
791
|
|
|
* reference is guaranteed to exist on the filesystem. |
792
|
|
|
*/ |
793
|
108 |
|
private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0) |
794
|
|
|
{ |
795
|
108 |
|
$result = array(); |
796
|
|
|
|
797
|
108 |
|
foreach ($references as $reference) { |
798
|
|
|
// Filter out null values |
799
|
|
|
// Links should be followed before calling this method |
800
|
108 |
|
if (null === $reference) { |
801
|
48 |
|
continue; |
802
|
|
|
} |
803
|
|
|
|
804
|
76 |
|
$nestedReference = rtrim($reference, '/').'/'.$nestedPath; |
805
|
|
|
|
806
|
76 |
|
if (file_exists($nestedReference)) { |
807
|
64 |
|
$result[] = $nestedReference; |
808
|
|
|
|
809
|
64 |
|
if ($flags & self::STOP_ON_FIRST) { |
810
|
76 |
|
return $result; |
811
|
|
|
} |
812
|
|
|
} |
813
|
|
|
} |
814
|
|
|
|
815
|
82 |
|
return $result; |
816
|
|
|
} |
817
|
|
|
|
818
|
|
|
/** |
819
|
|
|
* Resolves a list of references stored in the JSON. |
820
|
|
|
* |
821
|
|
|
* Each reference passed in can be: |
822
|
|
|
* |
823
|
|
|
* * `null` |
824
|
|
|
* * a link starting with `@` |
825
|
|
|
* * a filesystem path relative to the base directory |
826
|
|
|
* * an absolute filesystem path |
827
|
|
|
* |
828
|
|
|
* Each reference returned by this method can be: |
829
|
|
|
* |
830
|
|
|
* * `null` |
831
|
|
|
* * a link starting with `@` |
832
|
|
|
* * an absolute filesystem path |
833
|
|
|
* |
834
|
|
|
* Additionally, the results are guaranteed to be an array. If the |
835
|
|
|
* argument `$stopOnFirst` is set, that array has a maximum size of 1. |
836
|
|
|
* |
837
|
|
|
* @param string $path The mapped Puli path. |
838
|
|
|
* @param mixed $references The reference(s). |
839
|
|
|
* @param int $flags A bitwise combination of the flag constants in |
840
|
|
|
* this class. |
841
|
|
|
* |
842
|
|
|
* @return string[]|null[] The resolved references. |
843
|
|
|
*/ |
844
|
174 |
|
private function resolveReferences($path, $references, $flags = 0) |
845
|
|
|
{ |
846
|
174 |
|
$result = array(); |
847
|
|
|
|
848
|
174 |
|
if (!is_array($references)) { |
849
|
174 |
|
$references = array($references); |
850
|
|
|
} |
851
|
|
|
|
852
|
174 |
|
foreach ($references as $key => $reference) { |
853
|
|
|
// Keep non-filesystem references as they are |
854
|
174 |
|
if (!$this->isFilesystemReference($reference)) { |
855
|
76 |
|
$result[] = $reference; |
856
|
|
|
|
857
|
76 |
|
if ($flags & self::STOP_ON_FIRST) { |
858
|
26 |
|
return $result; |
859
|
|
|
} |
860
|
|
|
|
861
|
76 |
|
continue; |
862
|
|
|
} |
863
|
|
|
|
864
|
170 |
|
$absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory); |
865
|
170 |
|
$referenceExists = file_exists($absoluteReference); |
866
|
|
|
|
867
|
170 |
|
if (($flags & self::NO_CHECK_FILE_EXISTS) || $referenceExists) { |
868
|
170 |
|
$result[] = $absoluteReference; |
869
|
|
|
|
870
|
170 |
|
if ($flags & self::STOP_ON_FIRST) { |
871
|
52 |
|
return $result; |
872
|
|
|
} |
873
|
|
|
} |
874
|
|
|
|
875
|
170 |
|
if (!$referenceExists) { |
876
|
170 |
|
$this->logReferenceNotFound($path, $reference, $absoluteReference); |
877
|
|
|
} |
878
|
|
|
} |
879
|
|
|
|
880
|
174 |
|
return $result; |
881
|
|
|
} |
882
|
|
|
|
883
|
|
|
/** |
884
|
|
|
* Returns the depth of a Puli path. |
885
|
|
|
* |
886
|
|
|
* The depth is used in order to limit the recursion when recursively |
887
|
|
|
* iterating directories. |
888
|
|
|
* |
889
|
|
|
* The depth starts at 0 for the root: |
890
|
|
|
* |
891
|
|
|
* / 0 |
892
|
|
|
* /webmozart 1 |
893
|
|
|
* /webmozart/puli 2 |
894
|
|
|
* ... |
895
|
|
|
* |
896
|
|
|
* @param string $path A Puli path. |
897
|
|
|
* |
898
|
|
|
* @return int The depth starting with 0 for the root node. |
899
|
|
|
*/ |
900
|
26 |
|
private function getPathDepth($path) |
901
|
|
|
{ |
902
|
|
|
// / has depth 0 |
903
|
|
|
// /webmozart has depth 1 |
904
|
|
|
// /webmozart/puli has depth 2 |
905
|
|
|
// ... |
906
|
26 |
|
return substr_count(rtrim($path, '/'), '/'); |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
/** |
910
|
|
|
* Inserts a path at the beginning of the order list of a mapped path. |
911
|
|
|
* |
912
|
|
|
* @param string $path The path of the mapping where to prepend. |
913
|
|
|
* @param string $prependedPath The path of the mapping to prepend. |
914
|
|
|
*/ |
915
|
10 |
|
private function prependOrderEntry($path, $prependedPath) |
916
|
|
|
{ |
917
|
10 |
|
$lastEntry = reset($this->json['_order'][$path]); |
918
|
|
|
|
919
|
10 |
|
if ($prependedPath === $lastEntry['path']) { |
920
|
|
|
// If the first entry matches the new one, add the reference |
921
|
|
|
// of the current resource to the limit |
922
|
2 |
|
++$lastEntry['references']; |
923
|
|
|
} else { |
924
|
10 |
|
array_unshift($this->json['_order'][$path], array( |
925
|
10 |
|
'path' => $prependedPath, |
926
|
10 |
|
'references' => 1, |
927
|
|
|
)); |
928
|
|
|
} |
929
|
10 |
|
} |
930
|
|
|
|
931
|
|
|
/** |
932
|
|
|
* Initializes a path with the order of the closest parent path. |
933
|
|
|
* |
934
|
|
|
* @param string $path The path to initialize. |
935
|
|
|
* @param array $parentReferences The defined references for parent paths, |
936
|
|
|
* with long paths /a/b sorted before short |
937
|
|
|
* paths /a. |
938
|
|
|
*/ |
939
|
14 |
|
private function initWithParentOrder($path, array $parentReferences) |
940
|
|
|
{ |
941
|
14 |
|
foreach ($parentReferences as $parentPath => $_) { |
942
|
|
|
// Look for the first parent entry for which an order is defined |
943
|
14 |
|
if (isset($this->json['_order'][$parentPath])) { |
944
|
|
|
// Inherit that order |
945
|
2 |
|
$this->json['_order'][$path] = $this->json['_order'][$parentPath]; |
946
|
|
|
|
947
|
14 |
|
return; |
948
|
|
|
} |
949
|
|
|
} |
950
|
12 |
|
} |
951
|
|
|
|
952
|
|
|
/** |
953
|
|
|
* Initializes the order of a path with the default order. |
954
|
|
|
* |
955
|
|
|
* This is necessary if we want to insert a non-default order entry for |
956
|
|
|
* the first time. |
957
|
|
|
* |
958
|
|
|
* @param string $path The path to initialize. |
959
|
|
|
* @param string $insertedPath The path that is being inserted. |
960
|
|
|
* @param array $references The references for each defined path mapping |
961
|
|
|
* in the path chain. |
962
|
|
|
*/ |
963
|
10 |
|
private function initWithDefaultOrder($path, $insertedPath, $references) |
964
|
|
|
{ |
965
|
10 |
|
$this->json['_order'][$path] = array(); |
966
|
|
|
|
967
|
|
|
// Insert the default order, if none exists |
968
|
|
|
// i.e. long paths /a/b/c before short paths /a/b |
969
|
10 |
|
$parentPath = $path; |
970
|
|
|
|
971
|
10 |
|
while (true) { |
972
|
10 |
|
if (isset($references[$parentPath])) { |
973
|
|
|
$parentEntry = array( |
974
|
10 |
|
'path' => $parentPath, |
975
|
10 |
|
'references' => count($references[$parentPath]), |
976
|
|
|
); |
977
|
|
|
|
978
|
|
|
// Edge case: $parentPath equals $insertedPath. In this case we have |
979
|
|
|
// to subtract the entry that we're adding |
980
|
10 |
|
if ($parentPath === $insertedPath) { |
981
|
10 |
|
--$parentEntry['references']; |
982
|
|
|
} |
983
|
|
|
|
984
|
10 |
|
if (0 !== $parentEntry['references']) { |
985
|
10 |
|
$this->json['_order'][$path][] = $parentEntry; |
986
|
|
|
} |
987
|
|
|
} |
988
|
|
|
|
989
|
10 |
|
if ('/' === $parentPath) { |
990
|
10 |
|
break; |
991
|
|
|
} |
992
|
|
|
|
993
|
10 |
|
$parentPath = Path::getDirectory($parentPath); |
994
|
|
|
}; |
995
|
10 |
|
} |
996
|
|
|
} |
997
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.