Failed Conditions
Pull Request — 1.0 (#79)
by Bernhard
02:54
created

JsonRepository::followLinks()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 37
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 14.1433

Importance

Changes 5
Bugs 0 Features 1
Metric Value
c 5
b 0
f 1
dl 0
loc 37
ccs 9
cts 19
cp 0.4737
rs 6.7273
cc 7
eloc 16
nc 5
nop 2
crap 14.1433
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\EditableRepository;
16
use Puli\Repository\Api\Resource\PuliResource;
17
use Puli\Repository\Api\ResourceNotFoundException;
18
use Puli\Repository\ChangeStream\ResourceStack;
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 176
    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 176
        parent::__construct($path, $baseDirectory, $validateJson);
117 176
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 16
    public function getStack($path)
123
    {
124 16
        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 7
            $this->load();
126 7
        }
127
128 16
        $references = $this->searchReferences($path);
129
130 16
        if (!isset($references[$path])) {
131
            throw ResourceNotFoundException::forPath($path);
132
        }
133
134 16
        $resources = array();
135 16
        $pathReferences = $references[$path];
136
137
        // The first reference is the last (current) version
138
        // Hence traverse in reverse order
139 16
        for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) {
140 16
            $resources[] = $this->createResource($path, $ref);
141 16
        }
142
143 16
        return new ResourceStack($resources);
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 148
    protected function appendToChangeStream(PuliResource $resource)
150
    {
151 148
        $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 148
        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 148
        $references = $this->searchReferences(
163 148
            $path,
164
            // Don't do filesystem checks here. We only check the filesystem
165
            // when reading, not when adding.
166 148
            self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS
167
                // Include references for mapped ancestor and nested paths
168 148
                | self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED
169 148
        );
170
171
        // Filter virtual resources
172 148
        $references = array_filter($references, function ($currentReferences) {
173 148
            return array(null) !== $currentReferences;
174 148
        });
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 148
        $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 148
        if ($pos + 1 < count($references)) {
188
            // Inherit the parent order if necessary
189 12
            if (!isset($this->json['_order'][$path])) {
190 12
                $parentReferences = array_slice($references, $pos + 1);
191
192 12
                $this->initWithParentOrder($path, $parentReferences);
193 12
            }
194
195
            // A parent order was inherited. Insert the path itself.
196 12
            if (isset($this->json['_order'][$path])) {
197 2
                $this->prependOrderEntry($path, $path);
198 2
            }
199 12
        }
200
201
        // 2. If there are child mappings, insert the current path into their order
202
203 148
        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 10
            }
217
218
            // After initializing all order entries, insert the new one
219 10
            foreach ($subReferences as $subPath => $_) {
220 10
                $this->prependOrderEntry($subPath, $path);
221 10
            }
222 10
        }
223 148
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 148
    protected function insertReference($path, $reference)
229
    {
230 148
        if (!isset($this->json[$path])) {
231
            // Store first entries as simple reference
232 148
            $this->json[$path] = $reference;
233
234 148
            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 10
        }
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 10
        }
251 10
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 16
    protected function removeReferences($glob)
257
    {
258 16
        $checkResults = $this->getReferencesForGlob($glob);
259 16
        $nonDeletablePaths = array();
260
261 16
        foreach ($checkResults as $path => $filesystemPath) {
262 16
            if (!array_key_exists($path, $this->json)) {
263 2
                $nonDeletablePaths[] = $filesystemPath;
264 2
            }
265 16
        }
266
267 16
        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 2
                reset($nonDeletablePaths),
272 2
                count($nonDeletablePaths) > 1
273 2
                    ? ' and '.(count($nonDeletablePaths) - 1).' more'
274
                    : ''
275 2
            ));
276
        }
277
278 14
        $deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM);
279 14
        $removed = 0;
280
281 14
        foreach ($deletedPaths as $path => $filesystemPath) {
282 14
            $removed += 1 + count($this->getReferencesForGlob($path.'/**/*'));
283
284 14
            unset($this->json[$path]);
285 14
        }
286
287 14
        return $removed;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 98
    protected function getReferencesForPath($path)
294
    {
295
        // Stop on first result and flatten
296 98
        return $this->flatten($this->searchReferences($path, true));
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302 40 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 40
        if (!Glob::isDynamic($glob)) {
305 28
            return $this->getReferencesForPath($glob);
306
        }
307
308 26
        return $this->getReferencesForRegex(
309 26
            Glob::getBasePath($glob),
310 26
            Glob::toRegEx($glob),
311
            $flags
312 26
        );
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318 50
    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0)
319
    {
320 50
        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 50
            $this->searchReferences($staticPrefix, self::INCLUDE_NESTED),
324 50
            $regex,
325 50
            $flags,
326
            $maxDepth
327 50
        );
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333 22 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 22
        $basePath = rtrim($path, '/');
336
337 22
        return $this->getReferencesForRegex(
338 22
            $basePath.'/',
339 22
            '~^'.preg_quote($basePath, '~').'/[^/]+$~',
340 22
            $flags,
341
            // Limit the directory exploration to the depth of the path + 1
342 22
            $this->getPathDepth($path) + 1
343 22
        );
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 98
    private function flatten(array $references)
367
    {
368 98
        $result = array();
369
370 98
        foreach ($references as $currentPath => $currentReferences) {
371 90
            if (!isset($result[$currentPath])) {
372 90
                $result[$currentPath] = reset($currentReferences);
373 90
            }
374 98
        }
375
376 98
        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 50
    private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0)
416
    {
417 50
        $result = array();
418
419 50
        foreach ($references as $currentPath => $currentReferences) {
420
            // Check whether the current entry matches the pattern
421 44 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...
422
                // If yes, the first stored reference is returned
423 18
                $result[$currentPath] = reset($currentReferences);
424
425 18
                if ($flags & self::STOP_ON_FIRST) {
426 2
                    return $result;
427
                }
428 16
            }
429
430 44
            if ($flags & self::NO_SEARCH_FILESYSTEM) {
431 14
                continue;
432
            }
433
434
            // First follow any links before we check which of them is a directory
435 44
            $currentReferences = $this->followLinks($currentReferences);
436 44
            $currentPath = rtrim($currentPath, '/');
437
438
            // Search the nested entries if desired
439 44
            foreach ($currentReferences as $baseFilesystemPath) {
440
                // Ignore null values and file paths
441 44
                if (!is_dir($baseFilesystemPath)) {
442 22
                    continue;
443
                }
444
445 24
                $iterator = new RecursiveIteratorIterator(
446 24
                    new RecursiveDirectoryIterator(
447 24
                        $baseFilesystemPath,
448
                        RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
449 24
                            | RecursiveDirectoryIterator::SKIP_DOTS
450 24
                    ),
451
                    RecursiveIteratorIterator::SELF_FIRST
452 24
                );
453
454 24
                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 14
                }
464
465 24
                $basePathLength = strlen($baseFilesystemPath);
466
467 24
                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)) {
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...
471 24
                        $result[$nestedPath] = $nestedFilesystemPath;
472
473 24
                        if ($flags & self::STOP_ON_FIRST) {
474 8
                            return $result;
475
                        }
476 18
                    }
477 22
                }
478 42
            }
479 48
        }
480
481 48
        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 150
    private function searchReferences($searchPath, $flags = 0)
524
    {
525 150
        $result = array();
526 150
        $foundMatchingMappings = false;
527 150
        $searchPath = rtrim($searchPath, '/');
528 150
        $searchPathForTest = $searchPath.'/';
529
530 150
        foreach ($this->json as $currentPath => $currentReferences) {
531 150
            $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 150 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...
536 150
                $foundMatchingMappings = true;
537 150
                $result[$currentPath] = $this->resolveReferences($currentReferences, $flags);
538
539
                // Return unless an explicit mapping order is defined
540
                // In that case, the ancestors need to be searched as well
541 150
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
542 46
                    return $result;
543
                }
544
545 150
                continue;
546
            }
547
548
            // We found a mapping that lies within the search path
549
            // e.g. mapping /a/b/c for path /a/b
550 110
            if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) {
551 26
                $foundMatchingMappings = true;
552 26
                $result[$currentPath] = $this->resolveReferences($currentReferences, $flags);
553
554
                // Return unless an explicit mapping order is defined
555
                // In that case, the ancestors need to be searched as well
556 26
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
557
                    return $result;
558
                }
559
560 26
                continue;
561
            }
562
563
            // We found a mapping that is an ancestor of the search path
564
            // e.g. mapping /a for path /a/b
565 110
            if (0 === strpos($searchPathForTest, $currentPathForTest)) {
566 110
                $foundMatchingMappings = true;
567
568 110 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...
569
                    // Include the references of the ancestor
570 52
                    $result[$currentPath] = $this->resolveReferences($currentReferences, $flags);
571
572
                    // Return unless an explicit mapping order is defined
573
                    // In that case, the ancestors need to be searched as well
574 52
                    if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
575
                        return $result;
576
                    }
577
578 52
                    continue;
579
                }
580
581 92
                if ($flags & self::NO_SEARCH_FILESYSTEM) {
582
                    continue;
583
                }
584
585
                // Check the filesystem directories pointed to by the ancestors
586
                // for the searched path
587 92
                $nestedPath = substr($searchPath, strlen($currentPathForTest));
588 92
                $currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath;
589
590
                // Follow links so that we can check the nested directories in
591
                // the final transitive link targets
592 92
                $currentReferencesResolved = $this->followLinks(
593
                    // Never stop on first, since appendNestedPath() might
594
                    // discard the first but accept the second entry
595 92
                    $this->resolveReferences($currentReferences, $flags & (~self::STOP_ON_FIRST)),
596
                    // Never stop on first (see above)
597
                    false
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
598 92
                );
599
600
                // Append the path and check which of the resulting paths exist
601 92
                $nestedReferences = $this->appendPathAndFilterExisting(
602 92
                    $currentReferencesResolved,
603 92
                    $nestedPath,
604
                    $flags
605 92
                );
606
607
                // None of the results exists
608 92
                if (empty($nestedReferences)) {
609 46
                    continue;
610
                }
611
612
                // Return unless an explicit mapping order is defined
613
                // In that case, the ancestors need to be searched as well
614 64
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) {
615
                    // The nested references already have size 1
616 50
                    return array($currentPathWithNested => $nestedReferences);
617
                }
618
619
                // We are traversing long keys before short keys
620
                // It could be that this entry already exists.
621 34
                if (!isset($result[$currentPathWithNested])) {
622 22
                    $result[$currentPathWithNested] = $nestedReferences;
623
624 22
                    continue;
625
                }
626
627
                // If no explicit mapping order is defined, simply append the
628
                // new references to the existing ones
629 12
                if (!isset($this->json['_order'][$currentPathWithNested])) {
630 2
                    $result[$currentPathWithNested] = array_merge(
631 2
                        $result[$currentPathWithNested],
632
                        $nestedReferences
633 2
                    );
634
635 2
                    continue;
636
                }
637
638
                // If an explicit mapping order is defined, store the paths
639
                // of the mappings that generated each reference set and
640
                // resolve the order later on
641 10
                if (!isset($result[$currentPathWithNested][$currentPathWithNested])) {
642 10
                    $result[$currentPathWithNested] = array(
643 10
                        $currentPathWithNested => $result[$currentPathWithNested],
644
                    );
645 10
                }
646
647
                // Add the new references generated by the current mapping
648 10
                $result[$currentPathWithNested][$currentPath] = $nestedReferences;
649
650 10
                continue;
651
            }
652
653
            // We did not find anything but previously found mappings
654
            // The mappings are sorted alphabetically, so we can safely abort
655 34
            if ($foundMatchingMappings) {
656 22
                break;
657
            }
658 150
        }
659
660
        // Resolve the order where it is explicitly set
661 150
        if (!isset($this->json['_order'])) {
662 150
            return $result;
663
        }
664
665 10
        foreach ($result as $currentPath => $referencesByMappedPath) {
666
            // If no order is defined for the path or if only one mapped path
667
            // generated references, there's nothing to do
668 10
            if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) {
669 6
                continue;
670
            }
671
672 10
            $orderedReferences = array();
673
674 10
            foreach ($this->json['_order'][$currentPath] as $orderEntry) {
675 10
                if (!isset($referencesByMappedPath[$orderEntry['path']])) {
676
                    continue;
677
                }
678
679 10
                for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) {
680 10
                    $orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]);
681 10
                }
682
683
                // Only include references of the first mapped path
684
                // Since $stopOnFirst is set, those references have a
685
                // maximum size of 1
686 10
                if ($flags & self::STOP_ON_FIRST) {
687
                    break;
688
                }
689 10
            }
690
691 10
            $result[$currentPath] = $orderedReferences;
692 10
        }
693
694 10
        return $result;
695
    }
696
697
    /**
698
     * Follows any link in a list of references.
699
     *
700
     * This method takes all the given references, checks for links starting
701
     * with "@" and recursively expands those links to their target references.
702
     * The target references may be `null` or absolute filesystem paths.
703
     *
704
     * Null values are returned unchanged.
705
     *
706
     * Absolute filesystem paths are returned unchanged.
707
     *
708
     * @param string[]|null[] $references The references.
709
     * @param int             $flags      A bitwise combination of the flag
710
     *                                    constants in this class.
711
     *
712
     * @return string[]|null[] The references with all links replaced by their
713
     *                         target references. If any link pointed to more
714
     *                         than one target reference, the returned array
715
     *                         is larger than the passed array (unless the
716
     *                         argument `$stopOnFirst` was set to `true`).
717
     */
718 96
    private function followLinks(array $references, $flags = 0)
719
    {
720 96
        $result = array();
721
722 96
        foreach ($references as $key => $reference) {
723
            // Not a link
724 96
            if (!$this->isLinkReference($reference)) {
725 96
                $result[] = $reference;
726
727 96
                if ($flags & self::STOP_ON_FIRST) {
728
                    return $result;
729
                }
730
731 96
                continue;
732
            }
733
734
            $referencedPath = substr($reference, 1);
735
736
            // Get all the file system paths that this link points to
737
            // and append them to the result
738
            foreach ($this->searchReferences($referencedPath, $flags) as $referencedReferences) {
739
                // Follow links recursively
740
                $referencedReferences = $this->followLinks($referencedReferences);
741
742
                // Append all resulting target paths to the result
743
                foreach ($referencedReferences as $referencedReference) {
744
                    $result[] = $referencedReference;
745
746
                    if ($flags & self::STOP_ON_FIRST) {
747
                        return $result;
748
                    }
749
                }
750
            }
751 96
        }
752
753 96
        return $result;
754
    }
755
756
    /**
757
     * Appends nested paths to references and filters out the existing ones.
758
     *
759
     * This method takes all the given references, appends the nested path to
760
     * each of them and then filters out the results that actually exist on the
761
     * filesystem.
762
     *
763
     * Null references are filtered out.
764
     *
765
     * Link references should be followed with {@link followLinks()} before
766
     * calling this method.
767
     *
768
     * @param string[]|null[] $references The references.
769
     * @param string          $nestedPath The nested path to append without
770
     *                                    leading slash ("/").
771
     * @param int             $flags      A bitwise combination of the flag
772
     *                                    constants in this class.
773
     *
774
     * @return string[] The references with the nested path appended. Each
775
     *                  reference is guaranteed to exist on the filesystem.
776
     */
777 92
    private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0)
778
    {
779 92
        $result = array();
780
781 92
        foreach ($references as $reference) {
782
            // Filter out null values
783
            // Links should be followed before calling this method
784 92
            if (null === $reference) {
785 32
                continue;
786
            }
787
788 74
            $nestedReference = rtrim($reference, '/').'/'.$nestedPath;
789
790 74
            if (file_exists($nestedReference)) {
791 64
                $result[] = $nestedReference;
792
793 64
                if ($flags & self::STOP_ON_FIRST) {
794 50
                    return $result;
795
                }
796 34
            }
797 66
        }
798
799 66
        return $result;
800
    }
801
802
    /**
803
     * Resolves a list of references stored in the JSON.
804
     *
805
     * Each reference passed in can be:
806
     *
807
     *  * `null`
808
     *  * a link starting with `@`
809
     *  * a filesystem path relative to the base directory
810
     *  * an absolute filesystem path
811
     *
812
     * Each reference returned by this method can be:
813
     *
814
     *  * `null`
815
     *  * a link starting with `@`
816
     *  * an absolute filesystem path
817
     *
818
     * Additionally, the results are guaranteed to be an array. If the
819
     * argument `$stopOnFirst` is set, that array has a maximum size of 1.
820
     *
821
     * @param mixed $references The reference(s).
822
     * @param int   $flags      A bitwise combination of the flag constants in
823
     *                          this class.
824
     *
825
     * @return string[]|null[] The resolved references.
826
     */
827 150
    private function resolveReferences($references, $flags = 0)
828
    {
829 150
        if (!is_array($references)) {
830 150
            $references = array($references);
831 150
        }
832
833 150
        foreach ($references as $key => $reference) {
834 150
            if ($this->isFilesystemReference($reference)) {
835 148
                $reference = Path::makeAbsolute($reference, $this->baseDirectory);
836
837
                // Ignore non-existing files. Not sure this is the right
838
                // thing to do.
839 148
                if (($flags & self::NO_CHECK_FILE_EXISTS) || file_exists($reference)) {
840 148
                    $references[$key] = $reference;
841 148
                }
842 148
            }
843
844 150
            if ($flags & self::STOP_ON_FIRST) {
845 46
                return $references;
846
            }
847 150
        }
848
849 150
        return $references;
850
    }
851
852
    /**
853
     * Returns the depth of a Puli path.
854
     *
855
     * The depth is used in order to limit the recursion when recursively
856
     * iterating directories.
857
     *
858
     * The depth starts at 0 for the root:
859
     *
860
     * /                0
861
     * /webmozart       1
862
     * /webmozart/puli  2
863
     * ...
864
     *
865
     * @param string $path A Puli path.
866
     *
867
     * @return int The depth starting with 0 for the root node.
868
     */
869 22
    private function getPathDepth($path)
870
    {
871
        // / has depth 0
872
        // /webmozart has depth 1
873
        // /webmozart/puli has depth 2
874
        // ...
875 22
        return substr_count(rtrim($path, '/'), '/');
876
    }
877
878
    /**
879
     * Inserts a path at the beginning of the order list of a mapped path.
880
     *
881
     * @param string $path          The path of the mapping where to prepend.
882
     * @param string $prependedPath The path of the mapping to prepend.
883
     */
884 10
    private function prependOrderEntry($path, $prependedPath)
885
    {
886 10
        $lastEntry = reset($this->json['_order'][$path]);
887
888 10
        if ($prependedPath === $lastEntry['path']) {
889
            // If the first entry matches the new one, add the reference
890
            // of the current resource to the limit
891 2
            ++$lastEntry['references'];
892 2
        } else {
893 10
            array_unshift($this->json['_order'][$path], array(
894 10
                'path' => $prependedPath,
895 10
                'references' => 1,
896 10
            ));
897
        }
898 10
    }
899
900
    /**
901
     * Initializes a path with the order of the closest parent path.
902
     *
903
     * @param string $path             The path to initialize.
904
     * @param array  $parentReferences The defined references for parent paths,
905
     *                                 with long paths /a/b sorted before short
906
     *                                 paths /a.
907
     */
908 12
    private function initWithParentOrder($path, array $parentReferences)
909
    {
910 12
        foreach ($parentReferences as $parentPath => $_) {
911
            // Look for the first parent entry for which an order is defined
912 12
            if (isset($this->json['_order'][$parentPath])) {
913
                // Inherit that order
914 2
                $this->json['_order'][$path] = $this->json['_order'][$parentPath];
915
916 2
                return;
917
            }
918 10
        }
919 10
    }
920
921
    /**
922
     * Initializes the order of a path with the default order.
923
     *
924
     * This is necessary if we want to insert a non-default order entry for
925
     * the first time.
926
     *
927
     * @param string $path         The path to initialize.
928
     * @param string $insertedPath The path that is being inserted.
929
     * @param array  $references   The references for each defined path mapping
930
     *                             in the path chain.
931
     */
932 10
    private function initWithDefaultOrder($path, $insertedPath, $references)
933
    {
934 10
        $this->json['_order'][$path] = array();
935
936
        // Insert the default order, if none exists
937
        // i.e. long paths /a/b/c before short paths /a/b
938 10
        $parentPath = $path;
939
940 10
        while (true) {
941 10
            if (isset($references[$parentPath])) {
942
                $parentEntry = array(
943 10
                    'path' => $parentPath,
944 10
                    'references' => count($references[$parentPath]),
945 10
                );
946
947
                // Edge case: $parentPath equals $insertedPath. In this case we have
948
                // to subtract the entry that we're adding
949 10
                if ($parentPath === $insertedPath) {
950 10
                    --$parentEntry['references'];
951 10
                }
952
953 10
                if (0 !== $parentEntry['references']) {
954 10
                    $this->json['_order'][$path][] = $parentEntry;
955 10
                }
956 10
            }
957
958 10
            if ('/' === $parentPath) {
959 10
                break;
960
            }
961
962 10
            $parentPath = Path::getDirectory($parentPath);
963 10
        };
964 10
    }
965
}
966