JsonRepository   D
last analyzed

Complexity

Total Complexity 113

Size/Duplication

Total Lines 950
Duplicated Lines 7.89 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 94.24%

Importance

Changes 16
Bugs 4 Features 3
Metric Value
wmc 113
c 16
b 4
f 3
lcom 1
cbo 7
dl 75
loc 950
ccs 262
cts 278
cp 0.9424
rs 4

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B getVersions() 0 23 4
C storeVersion() 0 75 9
B insertReference() 0 24 5
B removeReferences() 0 33 6
A getReferencesForPath() 0 5 1
A getReferencesForGlob() 12 12 2
A getReferencesForRegex() 0 11 1
A getReferencesInDirectory() 12 12 1
A flatten() 0 12 3
C flattenWithFilter() 15 68 14
D searchReferences() 36 189 33
C followLinks() 0 37 7
B appendPathAndFilterExisting() 0 24 5
D resolveReferences() 0 38 9
A getPathDepth() 0 8 1
A prependOrderEntry() 0 15 2
A initWithParentOrder() 0 12 3
B initWithDefaultOrder() 0 33 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like JsonRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JsonRepository, and based on these observations, apply Extract Interface, too.

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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->json of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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 114
    protected function getReferencesForPath($path)
294
    {
295
        // Stop on first result and flatten
296 114
        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)
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...
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)
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...
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 114
    private function flatten(array $references)
367
    {
368 114
        $result = array();
369
370 114
        foreach ($references as $currentPath => $currentReferences) {
371 106
            if (!isset($result[$currentPath])) {
372 106
                $result[$currentPath] = reset($currentReferences);
373
            }
374
        }
375
376 114
        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
     * All references that contain directory paths may be traversed recursively and
391
     * scanned for more paths matching the regular expression. This recursive
392
     * traversal can be limited by passing a `$maxDepth` (see {@link getPathDepth()}).
393
     * By default, this `$maxDepth` is equal to zero (no recursive scan).
394
     *
395
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
396
     *
397
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
398
     * paths actually exist on the filesystem.
399
     *
400
     * Each reference returned by this method can be:
401
     *
402
     *  * `null`
403
     *  * a link starting with `@`
404
     *  * an absolute filesystem path
405
     *
406
     * The keys of the returned array are Puli paths. Their order is undefined.
407
     *
408
     * @param array  $references A two-level reference array as returned by
409
     *                           {@link searchReferences()}.
410
     * @param string $regex      A regular expression used to filter Puli paths.
411
     * @param int    $flags      A bitwise combination of the flag constants in
412
     *                           this class.
413
     * @param int    $maxDepth   The maximum path depth when searching the
414
     *                           contents of directory references. If 0, the
415
     *                           depth is unlimited.
416
     *
417
     * @return string[]|null[] A one-level array of references with Puli paths
418
     *                         as keys.
419
     */
420 68
    private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0)
421
    {
422 68
        $result = array();
423
424 68
        foreach ($references as $currentPath => $currentReferences) {
425
            // Check whether the current entry matches the pattern
426 62 View Code Duplication
            if (!isset($result[$currentPath]) && preg_match($regex, $currentPath)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
427
                // If yes, the first stored reference is returned
428 28
                $result[$currentPath] = reset($currentReferences);
429
430 28
                if ($flags & self::STOP_ON_FIRST) {
431 2
                    return $result;
432
                }
433
            }
434
435 62
            if ($flags & self::NO_SEARCH_FILESYSTEM) {
436 20
                continue;
437
            }
438
439
            // First follow any links before we check which of them is a directory
440 62
            $currentReferences = $this->followLinks($currentReferences);
441 62
            $currentPath = rtrim($currentPath, '/');
442
443
            // Search the nested entries if desired
444 62
            foreach ($currentReferences as $baseFilesystemPath) {
445
                // Ignore null values and file paths
446 62
                if (!is_dir($baseFilesystemPath)) {
447 40
                    continue;
448
                }
449
450 26
                $iterator = new RecursiveIteratorIterator(
451 26
                    new RecursiveDirectoryIterator(
452
                        $baseFilesystemPath,
453 26
                        RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
454 26
                            | RecursiveDirectoryIterator::SKIP_DOTS
455
                    ),
456 26
                    RecursiveIteratorIterator::SELF_FIRST
457
                );
458
459 26
                if (0 !== $maxDepth) {
460 14
                    $currentDepth = $this->getPathDepth($currentPath);
461 14
                    $maxIteratorDepth = $maxDepth - $currentDepth;
462
463 14
                    if ($maxIteratorDepth < 1) {
464
                        continue;
465
                    }
466
467 14
                    $iterator->setMaxDepth($maxIteratorDepth);
468
                }
469
470 26
                $basePathLength = strlen($baseFilesystemPath);
471
472 26
                foreach ($iterator as $nestedFilesystemPath) {
473 24
                    $nestedPath = substr_replace($nestedFilesystemPath, $currentPath, 0, $basePathLength);
474
475 24 View Code Duplication
                    if (!isset($result[$nestedPath]) && preg_match($regex, $nestedPath)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
476 24
                        $result[$nestedPath] = $nestedFilesystemPath;
477
478 24
                        if ($flags & self::STOP_ON_FIRST) {
479 62
                            return $result;
480
                        }
481
                    }
482
                }
483
            }
484
        }
485
486 66
        return $result;
487
    }
488
489
    /**
490
     * Filters the JSON file for all references relevant to a given search path.
491
     *
492
     * The JSON is scanned starting with the longest mapped Puli path.
493
     *
494
     * If the search path is "/a/b", the result includes:
495
     *
496
     *  * The references of the mapped path "/a/b".
497
     *
498
     * If the flag `INCLUDE_ANCESTORS` is used, the result additionally
499
     * includes:
500
     *
501
     *  * The references of any mapped super path "/a" with the sub-path "/b"
502
     *    appended.
503
     *
504
     * If the flag `INCLUDE_NESTED` is used, the result additionally
505
     * includes:
506
     *
507
     *  * The references of any mapped sub path "/a/b/c".
508
     *
509
     * This is useful if you want to look for the children of "/a/b" or scan
510
     * all descendants for paths matching a given pattern.
511
     *
512
     * The result of this method is an array with two levels:
513
     *
514
     *  * The first level has Puli paths as keys.
515
     *  * The second level contains all references for that path, where the
516
     *    first reference has the highest, the last reference the lowest
517
     *    priority. The keys of the second level are integers. There may be
518
     *    holes between any two keys.
519
     *
520
     * The references of the second level contain:
521
     *
522
     *  * `null` values for virtual resources
523
     *  * strings starting with "@" for links
524
     *  * absolute filesystem paths for filesystem resources
525
     *
526
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
527
     *
528
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
529
     * paths actually exist on the filesystem.
530
     *
531
     * @param string $searchPath The path to search.
532
     * @param int    $flags      A bitwise combination of the flag constants in
533
     *                           this class.
534
     *
535
     * @return array An array with two levels.
536
     */
537 174
    private function searchReferences($searchPath, $flags = 0)
538
    {
539 174
        $result = array();
540 174
        $foundMatchingMappings = false;
541 174
        $searchPath = rtrim($searchPath, '/');
542 174
        $searchPathForTest = $searchPath.'/';
543
544 174
        foreach ($this->json as $currentPath => $currentReferences) {
545 174
            $currentPathForTest = rtrim($currentPath, '/').'/';
546
547
            // We found a mapping that matches the search path
548
            // e.g. mapping /a/b for path /a/b
549 174 View Code Duplication
            if ($searchPathForTest === $currentPathForTest) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
550 174
                $foundMatchingMappings = true;
551 174
                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);
552
553 174
                if (empty($currentReferences)) {
554 4
                    continue;
555
                }
556
557 174
                $result[$currentPath] = $currentReferences;
558
559
                // Return unless an explicit mapping order is defined
560
                // In that case, the ancestors need to be searched as well
561 174
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
562 62
                    return $result;
563
                }
564
565 174
                continue;
566
            }
567
568
            // We found a mapping that lies within the search path
569
            // e.g. mapping /a/b/c for path /a/b
570 130
            if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) {
571 42
                $foundMatchingMappings = true;
572 42
                $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);
573
574 42
                if (empty($currentReferences)) {
575 8
                    continue;
576
                }
577
578 34
                $result[$currentPath] = $currentReferences;
579
580
                // Return unless an explicit mapping order is defined
581
                // In that case, the ancestors need to be searched as well
582 34
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
583
                    return $result;
584
                }
585
586 34
                continue;
587
            }
588
589
            // We found a mapping that is an ancestor of the search path
590
            // e.g. mapping /a for path /a/b
591 130
            if (0 === strpos($searchPathForTest, $currentPathForTest)) {
592 130
                $foundMatchingMappings = true;
593
594 130 View Code Duplication
                if ($flags & self::INCLUDE_ANCESTORS) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
595
                    // Include the references of the ancestor
596 72
                    $currentReferences = $this->resolveReferences($currentPath, $currentReferences, $flags);
597
598 72
                    if (empty($currentReferences)) {
599
                        continue;
600
                    }
601
602 72
                    $result[$currentPath] = $currentReferences;
603
604
                    // Return unless an explicit mapping order is defined
605
                    // In that case, the ancestors need to be searched as well
606 72
                    if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
607
                        return $result;
608
                    }
609
610 72
                    continue;
611
                }
612
613 112
                if ($flags & self::NO_SEARCH_FILESYSTEM) {
614
                    continue;
615
                }
616
617
                // Check the filesystem directories pointed to by the ancestors
618
                // for the searched path
619 112
                $nestedPath = substr($searchPath, strlen($currentPathForTest));
620 112
                $currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath;
621
622
                // Follow links so that we can check the nested directories in
623
                // the final transitive link targets
624 112
                $currentReferencesResolved = $this->followLinks(
625
                    // Never stop on first, since appendNestedPath() might
626
                    // discard the first but accept the second entry
627 112
                    $this->resolveReferences($currentPath, $currentReferences, $flags & (~self::STOP_ON_FIRST))
628
                );
629
630
                // Append the path and check which of the resulting paths exist
631 112
                $nestedReferences = $this->appendPathAndFilterExisting(
632
                    $currentReferencesResolved,
633
                    $nestedPath,
634
                    $flags
635
                );
636
637
                // None of the results exists
638 112
                if (empty($nestedReferences)) {
639 62
                    continue;
640
                }
641
642
                // Return unless an explicit mapping order is defined
643
                // In that case, the ancestors need to be searched as well
644 68
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) {
645
                    // The nested references already have size 1
646 54
                    return array($currentPathWithNested => $nestedReferences);
647
                }
648
649
                // We are traversing long keys before short keys
650
                // It could be that this entry already exists.
651 34
                if (!isset($result[$currentPathWithNested])) {
652 22
                    $result[$currentPathWithNested] = $nestedReferences;
653
654 22
                    continue;
655
                }
656
657
                // If no explicit mapping order is defined, simply append the
658
                // new references to the existing ones
659 12
                if (!isset($this->json['_order'][$currentPathWithNested])) {
660 2
                    $result[$currentPathWithNested] = array_merge(
661 2
                        $result[$currentPathWithNested],
662
                        $nestedReferences
663
                    );
664
665 2
                    continue;
666
                }
667
668
                // If an explicit mapping order is defined, store the paths
669
                // of the mappings that generated each reference set and
670
                // resolve the order later on
671 10
                if (!isset($result[$currentPathWithNested][$currentPathWithNested])) {
672 10
                    $result[$currentPathWithNested] = array(
673 10
                        $currentPathWithNested => $result[$currentPathWithNested],
674
                    );
675
                }
676
677
                // Add the new references generated by the current mapping
678 10
                $result[$currentPathWithNested][$currentPath] = $nestedReferences;
679
680 10
                continue;
681
            }
682
683
            // We did not find anything but previously found mappings
684
            // The mappings are sorted alphabetically, so we can safely abort
685 40
            if ($foundMatchingMappings) {
686 40
                break;
687
            }
688
        }
689
690
        // Resolve the order where it is explicitly set
691 174
        if (!isset($this->json['_order'])) {
692 174
            return $result;
693
        }
694
695 10
        foreach ($result as $currentPath => $referencesByMappedPath) {
696
            // If no order is defined for the path or if only one mapped path
697
            // generated references, there's nothing to do
698 10
            if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) {
699 6
                continue;
700
            }
701
702 10
            $orderedReferences = array();
703
704 10
            foreach ($this->json['_order'][$currentPath] as $orderEntry) {
705 10
                if (!isset($referencesByMappedPath[$orderEntry['path']])) {
706
                    continue;
707
                }
708
709 10
                for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) {
710 10
                    $orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]);
711
                }
712
713
                // Only include references of the first mapped path
714
                // Since $stopOnFirst is set, those references have a
715
                // maximum size of 1
716 10
                if ($flags & self::STOP_ON_FIRST) {
717 10
                    break;
718
                }
719
            }
720
721 10
            $result[$currentPath] = $orderedReferences;
722
        }
723
724 10
        return $result;
725
    }
726
727
    /**
728
     * Follows any link in a list of references.
729
     *
730
     * This method takes all the given references, checks for links starting
731
     * with "@" and recursively expands those links to their target references.
732
     * The target references may be `null` or absolute filesystem paths.
733
     *
734
     * Null values are returned unchanged.
735
     *
736
     * Absolute filesystem paths are returned unchanged.
737
     *
738
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
739
     *
740
     * @param string[]|null[] $references The references.
741
     * @param int             $flags      A bitwise combination of the flag
742
     *                                    constants in this class.
743
     *
744
     * @return string[]|null[] The references with all links replaced by their
745
     *                         target references. If any link pointed to more
746
     *                         than one target reference, the returned array
747
     *                         is larger than the passed array (unless the
748
     *                         argument `$stopOnFirst` was set to `true`).
749
     */
750 122
    private function followLinks(array $references, $flags = 0)
751
    {
752 122
        $result = array();
753
754 122
        foreach ($references as $key => $reference) {
755
            // Not a link
756 122
            if (!$this->isLinkReference($reference)) {
757 122
                $result[] = $reference;
758
759 122
                if ($flags & self::STOP_ON_FIRST) {
760
                    return $result;
761
                }
762
763 122
                continue;
764
            }
765
766
            $referencedPath = substr($reference, 1);
767
768
            // Get all the file system paths that this link points to
769
            // and append them to the result
770
            foreach ($this->searchReferences($referencedPath, $flags) as $referencedReferences) {
771
                // Follow links recursively
772
                $referencedReferences = $this->followLinks($referencedReferences);
773
774
                // Append all resulting target paths to the result
775
                foreach ($referencedReferences as $referencedReference) {
776
                    $result[] = $referencedReference;
777
778
                    if ($flags & self::STOP_ON_FIRST) {
779
                        return $result;
780
                    }
781
                }
782
            }
783
        }
784
785 122
        return $result;
786
    }
787
788
    /**
789
     * Appends nested paths to references and filters out the existing ones.
790
     *
791
     * This method takes all the given references, appends the nested path to
792
     * each of them and then filters out the results that actually exist on the
793
     * filesystem.
794
     *
795
     * Null references are filtered out.
796
     *
797
     * Link references should be followed with {@link followLinks()} before
798
     * calling this method.
799
     *
800
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
801
     *
802
     * @param string[]|null[] $references The references.
803
     * @param string          $nestedPath The nested path to append without
804
     *                                    leading slash ("/").
805
     * @param int             $flags      A bitwise combination of the flag
806
     *                                    constants in this class.
807
     *
808
     * @return string[] The references with the nested path appended. Each
809
     *                  reference is guaranteed to exist on the filesystem.
810
     */
811 112
    private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0)
812
    {
813 112
        $result = array();
814
815 112
        foreach ($references as $reference) {
816
            // Filter out null values
817
            // Links should be followed before calling this method
818 112
            if (null === $reference) {
819 48
                continue;
820
            }
821
822 80
            $nestedReference = rtrim($reference, '/').'/'.$nestedPath;
823
824 80
            if (file_exists($nestedReference)) {
825 68
                $result[] = $nestedReference;
826
827 68
                if ($flags & self::STOP_ON_FIRST) {
828 80
                    return $result;
829
                }
830
            }
831
        }
832
833 82
        return $result;
834
    }
835
836
    /**
837
     * Resolves a list of references stored in the JSON.
838
     *
839
     * Each reference passed in can be:
840
     *
841
     *  * `null`
842
     *  * a link starting with `@`
843
     *  * a filesystem path relative to the base directory
844
     *  * an absolute filesystem path
845
     *
846
     * Each reference returned by this method can be:
847
     *
848
     *  * `null`
849
     *  * a link starting with `@`
850
     *  * an absolute filesystem path
851
     *
852
     * Additionally, the results are guaranteed to be an array.
853
     *
854
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
855
     * In that case, the results array has a maximum size of 1.
856
     *
857
     * The flag `NO_SEARCH_FILESYSTEM` may be used to check for whether the found
858
     * paths actually exist on the filesystem.
859
     *
860
     * @param string $path       The mapped Puli path.
861
     * @param mixed  $references The reference(s).
862
     * @param int    $flags      A bitwise combination of the flag constants in
863
     *                           this class.
864
     *
865
     * @return string[]|null[] The resolved references.
866
     */
867 174
    private function resolveReferences($path, $references, $flags = 0)
868
    {
869 174
        $result = array();
870
871 174
        if (!is_array($references)) {
872 174
            $references = array($references);
873
        }
874
875 174
        foreach ($references as $key => $reference) {
876
            // Keep non-filesystem references as they are
877 174
            if (!$this->isFilesystemReference($reference)) {
878 76
                $result[] = $reference;
879
880 76
                if ($flags & self::STOP_ON_FIRST) {
881 26
                    return $result;
882
                }
883
884 76
                continue;
885
            }
886
887 170
            $absoluteReference = Path::makeAbsolute($reference, $this->baseDirectory);
888 170
            $referenceExists = file_exists($absoluteReference);
889
890 170
            if (($flags & self::NO_CHECK_FILE_EXISTS) || $referenceExists) {
891 170
                $result[] = $absoluteReference;
892
893 170
                if ($flags & self::STOP_ON_FIRST) {
894 56
                    return $result;
895
                }
896
            }
897
898 170
            if (!$referenceExists) {
899 170
                $this->logReferenceNotFound($path, $reference, $absoluteReference);
900
            }
901
        }
902
903 174
        return $result;
904
    }
905
906
    /**
907
     * Returns the depth of a Puli path.
908
     *
909
     * The depth is used in order to limit the recursion when recursively
910
     * iterating directories.
911
     *
912
     * The depth starts at 0 for the root:
913
     *
914
     * /                0
915
     * /webmozart       1
916
     * /webmozart/puli  2
917
     * ...
918
     *
919
     * @param string $path A Puli path.
920
     *
921
     * @return int The depth starting with 0 for the root node.
922
     */
923 26
    private function getPathDepth($path)
924
    {
925
        // / has depth 0
926
        // /webmozart has depth 1
927
        // /webmozart/puli has depth 2
928
        // ...
929 26
        return substr_count(rtrim($path, '/'), '/');
930
    }
931
932
    /**
933
     * Inserts a path at the beginning of the order list of a mapped path.
934
     *
935
     * @param string $path          The path of the mapping where to prepend.
936
     * @param string $prependedPath The path of the mapping to prepend.
937
     */
938 10
    private function prependOrderEntry($path, $prependedPath)
939
    {
940 10
        $lastEntry = reset($this->json['_order'][$path]);
941
942 10
        if ($prependedPath === $lastEntry['path']) {
943
            // If the first entry matches the new one, add the reference
944
            // of the current resource to the limit
945 2
            ++$lastEntry['references'];
946
        } else {
947 10
            array_unshift($this->json['_order'][$path], array(
948 10
                'path' => $prependedPath,
949 10
                'references' => 1,
950
            ));
951
        }
952 10
    }
953
954
    /**
955
     * Initializes a path with the order of the closest parent path.
956
     *
957
     * @param string $path             The path to initialize.
958
     * @param array  $parentReferences The defined references for parent paths,
959
     *                                 with long paths /a/b sorted before short
960
     *                                 paths /a.
961
     */
962 14
    private function initWithParentOrder($path, array $parentReferences)
963
    {
964 14
        foreach ($parentReferences as $parentPath => $_) {
965
            // Look for the first parent entry for which an order is defined
966 14
            if (isset($this->json['_order'][$parentPath])) {
967
                // Inherit that order
968 2
                $this->json['_order'][$path] = $this->json['_order'][$parentPath];
969
970 14
                return;
971
            }
972
        }
973 12
    }
974
975
    /**
976
     * Initializes the order of a path with the default order.
977
     *
978
     * This is necessary if we want to insert a non-default order entry for
979
     * the first time.
980
     *
981
     * @param string $path         The path to initialize.
982
     * @param string $insertedPath The path that is being inserted.
983
     * @param array  $references   The references for each defined path mapping
984
     *                             in the path chain.
985
     */
986 10
    private function initWithDefaultOrder($path, $insertedPath, $references)
987
    {
988 10
        $this->json['_order'][$path] = array();
989
990
        // Insert the default order, if none exists
991
        // i.e. long paths /a/b/c before short paths /a/b
992 10
        $parentPath = $path;
993
994 10
        while (true) {
995 10
            if (isset($references[$parentPath])) {
996
                $parentEntry = array(
997 10
                    'path' => $parentPath,
998 10
                    'references' => count($references[$parentPath]),
999
                );
1000
1001
                // Edge case: $parentPath equals $insertedPath. In this case we have
1002
                // to subtract the entry that we're adding
1003 10
                if ($parentPath === $insertedPath) {
1004 10
                    --$parentEntry['references'];
1005
                }
1006
1007 10
                if (0 !== $parentEntry['references']) {
1008 10
                    $this->json['_order'][$path][] = $parentEntry;
1009
                }
1010
            }
1011
1012 10
            if ('/' === $parentPath) {
1013 10
                break;
1014
            }
1015
1016 10
            $parentPath = Path::getDirectory($parentPath);
1017
        };
1018 10
    }
1019
}
1020