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

JsonRepository::removeReferences()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6.003

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 33
ccs 22
cts 23
cp 0.9565
rs 8.439
cc 6
eloc 20
nc 9
nop 1
crap 6.003
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 99
    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 99
        parent::__construct($path, $baseDirectory, $validateJson);
117 99
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 8
    public function getStack($path)
123
    {
124 8
        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 8
        $references = $this->searchReferences($path);
129
130 8
        if (!isset($references[$path])) {
131
            throw ResourceNotFoundException::forPath($path);
132
        }
133
134 8
        $resources = array();
135 8
        $pathReferences = $references[$path];
136
137
        // The first reference is the last (current) version
138
        // Hence traverse in reverse order
139 8
        for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) {
140 8
            $resources[] = $this->createResource($path, $ref);
141 8
        }
142
143 8
        return new ResourceStack($resources);
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 81
    protected function appendToChangeStream(PuliResource $resource)
150
    {
151 81
        $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 81
        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 81
        $references = $this->searchReferences(
163 81
            $path,
164
            // Don't do filesystem checks here. We only check the filesystem
165
            // when reading, not when adding.
166 81
            self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS
167
                // Include references for mapped ancestor and nested paths
168 81
                | self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED
169 81
        );
170
171
        // Filter virtual resources
172 81
        $references = array_filter($references, function ($currentReferences) {
173 81
            return array(null) !== $currentReferences;
174 81
        });
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 81
        $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 81
        if ($pos + 1 < count($references)) {
188
            // Inherit the parent order if necessary
189 8
            if (!isset($this->json['_order'][$path])) {
190 8
                $parentReferences = array_slice($references, $pos + 1);
191
192 8
                $this->initWithParentOrder($path, $parentReferences);
193 8
            }
194
195
            // A parent order was inherited. Insert the path itself.
196 8
            if (isset($this->json['_order'][$path])) {
197 1
                $this->prependOrderEntry($path, $path);
198 1
            }
199 8
        }
200
201
        // 2. If there are child mappings, insert the current path into their order
202
203 81
        if ($pos > 0) {
204 6
            $subReferences = array_slice($references, 0, $pos);
205
206 6
            foreach ($subReferences as $subPath => $_) {
207 6
                if (isset($this->json['_order'][$subPath])) {
208 3
                    continue;
209
                }
210
211 6
                if (isset($this->json['_order'][$path])) {
212
                    $this->json['_order'][$subPath] = $this->json['_order'][$path];
213
                } else {
214 6
                    $this->initWithDefaultOrder($subPath, $path, $references);
215
                }
216 6
            }
217
218
            // After initializing all order entries, insert the new one
219 6
            foreach ($subReferences as $subPath => $_) {
220 6
                $this->prependOrderEntry($subPath, $path);
221 6
            }
222 6
        }
223 81
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 81
    protected function insertReference($path, $reference)
229
    {
230 81
        if (!isset($this->json[$path])) {
231
            // Store first entries as simple reference
232 81
            $this->json[$path] = $reference;
233
234 81
            return;
235
        }
236
237 7
        if ($reference === $this->json[$path]) {
238
            // Reference is already set
239 2
            return;
240
        }
241
242 5
        if (!is_array($this->json[$path])) {
243
            // Convert existing entries to arrays for follow ups
244 5
            $this->json[$path] = array($this->json[$path]);
245 5
        }
246
247 5
        if (!in_array($reference, $this->json[$path], true)) {
248
            // Insert at the beginning of the array
249 5
            array_unshift($this->json[$path], $reference);
250 5
        }
251 5
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 8
    protected function removeReferences($glob)
257
    {
258 8
        $checkResults = $this->getReferencesForGlob($glob);
259 8
        $nonDeletablePaths = array();
260
261 8
        foreach ($checkResults as $path => $filesystemPath) {
262 8
            if (!array_key_exists($path, $this->json)) {
263 1
                $nonDeletablePaths[] = $filesystemPath;
264 1
            }
265 8
        }
266
267 8
        if (count($nonDeletablePaths) > 0) {
268 1
            throw new InvalidArgumentException(sprintf(
269
                'You cannot remove resources that are not mapped in the JSON '.
270 1
                'file. Tried to remove %s%s.',
271 1
                reset($nonDeletablePaths),
272 1
                count($nonDeletablePaths) > 1
273 1
                    ? ' and '.(count($nonDeletablePaths) - 1).' more'
274
                    : ''
275 1
            ));
276
        }
277
278 7
        $deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM);
279 7
        $removed = 0;
280
281 7
        foreach ($deletedPaths as $path => $filesystemPath) {
282 7
            $removed += 1 + count($this->getReferencesForGlob($path.'/**/*'));
283
284 7
            unset($this->json[$path]);
285 7
        }
286
287 7
        return $removed;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 56
    protected function getReferencesForPath($path)
294
    {
295
        // Stop on first result and flatten
296 56
        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 27 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 27
        if (!Glob::isDynamic($glob)) {
305 19
            return $this->getReferencesForPath($glob);
306
        }
307
308 15
        return $this->getReferencesForRegex(
309 15
            Glob::getBasePath($glob),
310 15
            Glob::toRegEx($glob),
311
            $flags
312 15
        );
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318 32
    protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0)
319
    {
320 32
        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 32
            $this->searchReferences($staticPrefix, self::INCLUDE_NESTED),
324 32
            $regex,
325 32
            $flags,
326
            $maxDepth
327 32
        );
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333 16 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 16
        $basePath = rtrim($path, '/');
336
337 16
        return $this->getReferencesForRegex(
338 16
            $basePath.'/',
339 16
            '~^'.preg_quote($basePath, '~').'/[^/]+$~',
340 16
            $flags,
341
            // Limit the directory exploration to the depth of the path + 1
342 16
            $this->getPathDepth($path) + 1
343 16
        );
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 56
    private function flatten(array $references)
367
    {
368 56
        $result = array();
369
370 56
        foreach ($references as $currentPath => $currentReferences) {
371 52
            if (!isset($result[$currentPath])) {
372 52
                $result[$currentPath] = reset($currentReferences);
373 52
            }
374 56
        }
375
376 56
        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 32
    private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0)
416
    {
417 32
        $result = array();
418
419 32
        foreach ($references as $currentPath => $currentReferences) {
420
            // Check whether the current entry matches the pattern
421 29 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 13
                $result[$currentPath] = reset($currentReferences);
424
425 13
                if ($flags & self::STOP_ON_FIRST) {
426 1
                    return $result;
427
                }
428 12
            }
429
430 29
            if ($flags & self::NO_SEARCH_FILESYSTEM) {
431 7
                continue;
432
            }
433
434
            // First follow any links before we check which of them is a directory
435 29
            $currentReferences = $this->followLinks($currentReferences);
436 29
            $currentPath = rtrim($currentPath, '/');
437
438
            // Search the nested entries if desired
439 29
            foreach ($currentReferences as $baseFilesystemPath) {
440
                // Ignore null values and file paths
441 29
                if (!is_dir($baseFilesystemPath)) {
442 16
                    continue;
443
                }
444
445 19
                $iterator = new RecursiveIteratorIterator(
446 19
                    new RecursiveDirectoryIterator(
447 19
                        $baseFilesystemPath,
448
                        RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
449 19
                            | RecursiveDirectoryIterator::SKIP_DOTS
450 19
                    ),
451
                    RecursiveIteratorIterator::SELF_FIRST
452 19
                );
453
454 19
                if (0 !== $maxDepth) {
455 12
                    $currentDepth = $this->getPathDepth($currentPath);
456 12
                    $maxIteratorDepth = $maxDepth - $currentDepth;
457
458 12
                    if ($maxIteratorDepth < 1) {
459 1
                        continue;
460
                    }
461
462 12
                    $iterator->setMaxDepth($maxIteratorDepth);
463 12
                }
464
465 19
                $basePathLength = strlen($baseFilesystemPath);
466
467 19
                foreach ($iterator as $nestedFilesystemPath) {
468 19
                    $nestedPath = substr_replace($nestedFilesystemPath, $currentPath, 0, $basePathLength);
469
470 19 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 19
                        $result[$nestedPath] = $nestedFilesystemPath;
472
473 19
                        if ($flags & self::STOP_ON_FIRST) {
474 4
                            return $result;
475
                        }
476 16
                    }
477 18
                }
478 28
            }
479 31
        }
480
481 31
        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 87
    private function searchReferences($searchPath, $flags = 0)
524
    {
525 87
        $result = array();
526 87
        $foundMatchingMappings = false;
527 87
        $searchPath = rtrim($searchPath, '/');
528 87
        $searchPathForTest = $searchPath.'/';
529
530 87
        foreach ($this->json as $currentPath => $currentReferences) {
531 87
            $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 87 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 84
                $foundMatchingMappings = true;
537 84
                $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 84
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
542 29
                    return $result;
543
                }
544
545 83
                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 67
            if (($flags & self::INCLUDE_NESTED) && 0 === strpos($currentPathForTest, $searchPathForTest)) {
551 20
                $foundMatchingMappings = true;
552 20
                $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 20
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
557
                    return $result;
558
                }
559
560 20
                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 67
            if (0 === strpos($searchPathForTest, $currentPathForTest)) {
566 66
                $foundMatchingMappings = true;
567
568 66 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 33
                    $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 33
                    if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPath])) {
575
                        return $result;
576
                    }
577
578 33
                    continue;
579
                }
580
581 54
                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 54
                $nestedPath = substr($searchPath, strlen($currentPathForTest));
588 54
                $currentPathWithNested = rtrim($currentPath, '/').'/'.$nestedPath;
589
590
                // Follow links so that we can check the nested directories in
591
                // the final transitive link targets
592 54
                $currentReferencesResolved = $this->followLinks(
593
                    // Never stop on first, since appendNestedPath() might
594
                    // discard the first but accept the second entry
595 54
                    $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 54
                );
599
600
                // Append the path and check which of the resulting paths exist
601 54
                $nestedReferences = $this->appendPathAndFilterExisting(
602 54
                    $currentReferencesResolved,
603 54
                    $nestedPath,
604
                    $flags
605 54
                );
606
607
                // None of the results exists
608 54
                if (empty($nestedReferences)) {
609 29
                    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 39
                if (($flags & self::STOP_ON_FIRST) && !isset($this->json['_order'][$currentPathWithNested])) {
615
                    // The nested references already have size 1
616 29
                    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 21
                if (!isset($result[$currentPathWithNested])) {
622 15
                    $result[$currentPathWithNested] = $nestedReferences;
623
624 15
                    continue;
625
                }
626
627
                // If no explicit mapping order is defined, simply append the
628
                // new references to the existing ones
629 6
                if (!isset($this->json['_order'][$currentPathWithNested])) {
630 1
                    $result[$currentPathWithNested] = array_merge(
631 1
                        $result[$currentPathWithNested],
632
                        $nestedReferences
633 1
                    );
634
635 1
                    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 5
                if (!isset($result[$currentPathWithNested][$currentPathWithNested])) {
642 5
                    $result[$currentPathWithNested] = array(
643 5
                        $currentPathWithNested => $result[$currentPathWithNested],
644
                    );
645 5
                }
646
647
                // Add the new references generated by the current mapping
648 5
                $result[$currentPathWithNested][$currentPath] = $nestedReferences;
649
650 5
                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 21
            if ($foundMatchingMappings) {
656 11
                break;
657
            }
658 87
        }
659
660
        // Resolve the order where it is explicitly set
661 86
        if (!isset($this->json['_order'])) {
662 86
            return $result;
663
        }
664
665 6
        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 5
            if (!isset($this->json['_order'][$currentPath]) || !isset($referencesByMappedPath[$currentPath])) {
669 3
                continue;
670
            }
671
672 5
            $orderedReferences = array();
673
674 5
            foreach ($this->json['_order'][$currentPath] as $orderEntry) {
675 5
                if (!isset($referencesByMappedPath[$orderEntry['path']])) {
676
                    continue;
677
                }
678
679 5
                for ($i = 0; $i < $orderEntry['references'] && count($referencesByMappedPath[$orderEntry['path']]) > 0; ++$i) {
680 5
                    $orderedReferences[] = array_shift($referencesByMappedPath[$orderEntry['path']]);
681 5
                }
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 5
                if ($flags & self::STOP_ON_FIRST) {
687
                    break;
688
                }
689 5
            }
690
691 5
            $result[$currentPath] = $orderedReferences;
692 6
        }
693
694 6
        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 57
    private function followLinks(array $references, $flags = 0)
719
    {
720 57
        $result = array();
721
722 57
        foreach ($references as $key => $reference) {
723
            // Not a link
724 57
            if (!$this->isLinkReference($reference)) {
725 57
                $result[] = $reference;
726
727 57
                if ($flags & self::STOP_ON_FIRST) {
728
                    return $result;
729
                }
730
731 57
                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 57
        }
752
753 57
        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 54
    private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0)
778
    {
779 54
        $result = array();
780
781 54
        foreach ($references as $reference) {
782
            // Filter out null values
783
            // Links should be followed before calling this method
784 54
            if (null === $reference) {
785 22
                continue;
786
            }
787
788 45
            $nestedReference = rtrim($reference, '/').'/'.$nestedPath;
789
790 45
            if (file_exists($nestedReference)) {
791 39
                $result[] = $nestedReference;
792
793 39
                if ($flags & self::STOP_ON_FIRST) {
794 29
                    return $result;
795
                }
796 21
            }
797 39
        }
798
799 39
        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 87
    private function resolveReferences($references, $flags = 0)
828
    {
829 87
        if (!is_array($references)) {
830 87
            $references = array($references);
831 87
        }
832
833 87
        foreach ($references as $key => $reference) {
834 87
            if ($this->isFilesystemReference($reference)) {
835 86
                $reference = Path::makeAbsolute($reference, $this->baseDirectory);
836
837
                // Ignore non-existing files. Not sure this is the right
838
                // thing to do.
839 86
                if (($flags & self::NO_CHECK_FILE_EXISTS) || file_exists($reference)) {
840 86
                    $references[$key] = $reference;
841 86
                }
842 86
            }
843
844 87
            if ($flags & self::STOP_ON_FIRST) {
845 29
                return $references;
846
            }
847 86
        }
848
849 86
        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 16
    private function getPathDepth($path)
870
    {
871
        // / has depth 0
872
        // /webmozart has depth 1
873
        // /webmozart/puli has depth 2
874
        // ...
875 16
        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 6
    private function prependOrderEntry($path, $prependedPath)
885
    {
886 6
        $lastEntry = reset($this->json['_order'][$path]);
887
888 6
        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 1
            ++$lastEntry['references'];
892 1
        } else {
893 6
            array_unshift($this->json['_order'][$path], array(
894 6
                'path' => $prependedPath,
895 6
                'references' => 1,
896 6
            ));
897
        }
898 6
    }
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 8
    private function initWithParentOrder($path, array $parentReferences)
909
    {
910 8
        foreach ($parentReferences as $parentPath => $_) {
911
            // Look for the first parent entry for which an order is defined
912 8
            if (isset($this->json['_order'][$parentPath])) {
913
                // Inherit that order
914 1
                $this->json['_order'][$path] = $this->json['_order'][$parentPath];
915
916 1
                return;
917
            }
918 7
        }
919 7
    }
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 6
    private function initWithDefaultOrder($path, $insertedPath, $references)
933
    {
934 6
        $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 6
        $parentPath = $path;
939
940 6
        while (true) {
941 6
            if (isset($references[$parentPath])) {
942
                $parentEntry = array(
943 6
                    'path' => $parentPath,
944 6
                    'references' => count($references[$parentPath]),
945 6
                );
946
947
                // Edge case: $parentPath equals $insertedPath. In this case we have
948
                // to subtract the entry that we're adding
949 6
                if ($parentPath === $insertedPath) {
950 6
                    --$parentEntry['references'];
951 6
                }
952
953 6
                if (0 !== $parentEntry['references']) {
954 6
                    $this->json['_order'][$path][] = $parentEntry;
955 6
                }
956 6
            }
957
958 6
            if ('/' === $parentPath) {
959 6
                break;
960
            }
961
962 6
            $parentPath = Path::getDirectory($parentPath);
963 6
        };
964 6
    }
965
}
966