Failed Conditions
Pull Request — 1.0 (#64)
by Titouan
05:04 queued 01:18
created

PathMappingRepository::followLinks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 ArrayIterator;
15
use BadMethodCallException;
16
use FilesystemIterator;
17
use Puli\Repository\Api\EditableRepository;
18
use Puli\Repository\Api\Resource\FilesystemResource;
19
use Puli\Repository\Api\Resource\PuliResource;
20
use Puli\Repository\Api\ResourceNotFoundException;
21
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
22
use Puli\Repository\Resource\LinkResource;
23
use RecursiveDirectoryIterator;
24
use RecursiveIteratorIterator;
25
use Webmozart\Assert\Assert;
26
use Webmozart\Glob\Glob;
27
use Webmozart\Glob\Iterator\RegexFilterIterator;
28
use Webmozart\PathUtil\Path;
29
30
/**
31
 * A development path mapping resource repository.
32
 * Each resource is resolved at `get()` time to improve
33
 * developer experience.
34
 *
35
 * Resources can be added with the method {@link add()}:
36
 *
37
 * ```php
38
 * use Puli\Repository\PathMappingRepository;
39
 *
40
 * $repo = new PathMappingRepository();
41
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
42
 * ```
43
 *
44
 * This repository only supports instances of FilesystemResource.
45
 *
46
 * @since  1.0
47
 *
48
 * @author Bernhard Schussek <[email protected]>
49
 * @author Titouan Galopin <[email protected]>
50
 */
51
class PathMappingRepository extends AbstractPathMappingRepository implements EditableRepository
52
{
53
    /**
54
     * {@inheritdoc}
55
     */
56 40
    public function get($path)
57
    {
58 40
        $path = $this->sanitizePath($path);
59 37
        $filesystemPaths = $this->resolveFilesystemPaths($path);
60
61 37
        if (0 === count($filesystemPaths)) {
62 2
            return $this->followLinksOrFail($path);
63
        }
64
65 36
        return $this->createResource(reset($filesystemPaths), $path);
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 20
    public function find($query, $language = 'glob')
72
    {
73 20
        $this->validateSearchLanguage($language);
74 19
        $query = $this->sanitizePath($query);
75
76 16
        return $this->search($query);
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 21
    public function contains($query, $language = 'glob')
83
    {
84 21
        $this->validateSearchLanguage($language);
85 20
        $query = $this->sanitizePath($query);
86
87 17
        return !$this->search($query, true)->isEmpty();
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 12
    public function remove($query, $language = 'glob')
94
    {
95 12
        $query = $this->sanitizePath($query);
96
97 9
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
98
99 7
        $results = $this->find($query, $language);
100 7
        $invalid = array();
101
102 7
        foreach ($results as $result) {
103 7
            if (!$this->store->exists($result->getPath())) {
104
                $invalid[] = $result->getFilesystemPath();
105
            }
106 7
        }
107
108 7
        if (count($invalid) === 1) {
109
            throw new BadMethodCallException(sprintf(
110
                'The remove query "%s" matched a resource that is not a path mapping', $query
111
            ));
112 7
        } elseif (count($invalid) > 1) {
113
            throw new BadMethodCallException(sprintf(
114
                'The remove query "%s" matched %s resources that are not path mappings', $query, count($invalid)
115
            ));
116
        }
117
118 7
        $removed = 0;
119
120 7
        foreach ($results as $result) {
121 7
            foreach ($this->getVirtualPathChildren($result->getPath(), true) as $virtualChild) {
122 5
                if ($this->store->remove($virtualChild['path'])) {
123 5
                    ++$removed;
124 5
                }
125 7
            }
126
127 7
            if ($this->store->remove($result->getPath())) {
128 7
                ++$removed;
129 7
            }
130 7
        }
131
132 7
        return $removed;
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 13
    public function listChildren($path)
139
    {
140 13
        if (!$this->isPathResolvable($path)) {
141 1
            throw ResourceNotFoundException::forPath($path);
142
        }
143
144 9
        return $this->getDirectChildren($this->sanitizePath($path));
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 7
    public function hasChildren($path)
151
    {
152 7
        if (!$this->isPathResolvable($path)) {
153 1
            throw ResourceNotFoundException::forPath($path);
154
        }
155
156 3
        return !$this->getDirectChildren($this->sanitizePath($path), true)->isEmpty();
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 70
    protected function addFilesystemResource($path, FilesystemResource $resource)
163
    {
164 70
        $resource->attachTo($this, $path);
165 70
        $this->storeUnshift($path, Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory));
166 70
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 3
    protected function addLinkResource($path, LinkResource $resource)
172
    {
173 3
        $resource->attachTo($this, $path);
174 3
        $this->storeUnshift($path, 'l:'.$resource->getTargetPath());
175 3
    }
176
177
    /**
178
     * Add a target path (link or filesystem path) to the beginning of the stack in the store at a path.
179
     *
180
     * @param string $path
181
     * @param string $targetPath
182
     */
183 70
    private function storeUnshift($path, $targetPath)
184
    {
185 70
        $previousPaths = array();
186
187 70
        if ($this->store->exists($path)) {
188 48
            $previousPaths = (array) $this->store->get($path);
189 48
        }
190
191 70
        if (!in_array($targetPath, $previousPaths, true)) {
192 70
            array_unshift($previousPaths, $targetPath);
193 70
        }
194
195 70
        $this->store->set($path, $previousPaths);
196 70
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 2
    protected function followLinks($path)
202
    {
203 2
        return $this->followLinksInternal($this->store->getMultiple($this->store->keys()), $path);
204
    }
205
206
    /**
207
     * Internal method to avoid repeated accesses to the store.
208
     *
209
     * @param array  $storePaths
210
     * @param string $path
211
     *
212
     * @return null|PuliResource
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use Resource\GenericResource|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
213
     */
214 2
    private function followLinksInternal($storePaths, $path)
215
    {
216 2
        foreach ($storePaths as $resourcePath => $filesystemPaths) {
217 2
            if (!is_array($filesystemPaths)) {
218 1
                continue;
219
            }
220
221 2
            if (0 !== strpos($path, $resourcePath)) {
222 1
                continue;
223
            }
224
225 2
            foreach ($filesystemPaths as $filesystemPath) {
226 2
                if (0 === strpos($filesystemPath, 'l:')) {
227 1
                    $targetPath = rtrim(substr($filesystemPath, 2), '/');
228 1
                    $realPath = substr_replace($path, $targetPath, 0, strlen($resourcePath));
229
230 1
                    $resolved = $this->resolveFilesystemPaths($realPath);
231
232 1
                    if (count($resolved) > 0) {
233 1
                        return $this->createResource(reset($resolved), $path);
0 ignored issues
show
Security Bug introduced by
It seems like reset($resolved) targeting reset() can also be of type false; however, Puli\Repository\Abstract...itory::createResource() does only seem to accept string|null, did you maybe forget to handle an error condition?
Loading history...
234
                    }
235
236 1
                    $nestedRealPath = $this->followLinksInternal($storePaths, $realPath);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $nestedRealPath is correct as $this->followLinksIntern...$storePaths, $realPath) (which targets Puli\Repository\PathMapp...::followLinksInternal()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
237
238 1
                    if ($nestedRealPath) {
239 1
                        return $nestedRealPath;
240
                    }
241
                }
242 1
            }
243 1
        }
244
245 1
        return null;
246
    }
247
248
    /**
249
     * Return the filesystem path associated to the given repository path
250
     * or null if no filesystem path is found.
251
     *
252
     * @param string $path      The repository path.
253
     * @param bool   $onlyFirst Should the method stop on the first path found?
254
     *
255
     * @return string[]|null[]
256
     */
257 58
    private function resolveFilesystemPaths($path, $onlyFirst = true)
258
    {
259
        /*
260
         * If the path exists in the store, return it directly
261
         */
262 58
        if ($this->store->exists($path)) {
263 36
            $filesystemPaths = $this->store->get($path);
264
265 36
            if (is_array($filesystemPaths) && count($filesystemPaths) > 0) {
266 33
                if ($onlyFirst) {
267 32
                    return $this->resolveRelativePaths(array(reset($filesystemPaths)));
268
                }
269
270 7
                return $this->resolveRelativePaths($filesystemPaths);
271
            }
272
273 12
            return array(null);
274
        }
275
276
        /*
277
         * Otherwise, we need to "resolve" it in two steps:
278
         *      1.  find the resources from the store that are potential parents
279
         *          of the path (we filter them using Path::isBasePath)
280
         *      2.  for each of these potential parent, we try to find a real
281
         *          file or directory on the filesystem and if we do find one,
282
         *          we stop
283
         */
284 41
        $basePaths = array_reverse($this->store->keys());
285 41
        $filesystemPaths = array();
286
287 41
        foreach ($basePaths as $key => $basePath) {
288 41
            if (!Path::isBasePath($basePath, $path)) {
289 3
                continue;
290
            }
291
292 41
            $filesystemBasePaths = $this->resolveRelativePaths((array) $this->store->get($basePath));
293 41
            $basePathLength = strlen(rtrim($basePath, '/').'/');
294
295 41
            foreach ($filesystemBasePaths as $filesystemBasePath) {
296 33
                $filesystemBasePath = rtrim($filesystemBasePath, '/').'/';
297 33
                $filesystemPath = substr_replace($path, $filesystemBasePath, 0, $basePathLength);
298
299
                // File
300 33
                if (file_exists($filesystemPath)) {
301 29
                    $filesystemPaths[] = $filesystemPath;
302
303 29
                    if ($onlyFirst) {
304 28
                        return $this->resolveRelativePaths($filesystemPaths);
305
                    }
306 11
                }
307 27
            }
308 26
        }
309
310 26
        return $this->resolveRelativePaths($filesystemPaths);
311
    }
312
313
    /**
314
     * Search for resources by querying their path.
315
     *
316
     * @param string $query        The glob query.
317
     * @param bool   $singleResult Should this method stop after finding a
318
     *                             first result, for performances.
319
     *
320
     * @return ArrayResourceCollection The results of search.
321
     */
322 26
    private function search($query, $singleResult = false)
323
    {
324 26
        $resources = new ArrayResourceCollection();
325
326
        // If the query is not a glob, return it directly
327 26
        if (!Glob::isDynamic($query)) {
328 18
            $filesystemPaths = $this->resolveFilesystemPaths($query);
329
330 18
            if (count($filesystemPaths) > 0) {
331 18
                $resources->add($this->createResource(reset($filesystemPaths), $query));
332 18
            }
333
334 18
            return $resources;
335
        }
336
337
        // If the glob is dynamic, we search
338 10
        $children = $this->getRecursiveChildren(Glob::getBasePath($query));
339
340 10
        foreach ($children as $path => $filesystemPath) {
341 9
            if (Glob::match($path, $query)) {
342 9
                $resources->add($this->createResource($filesystemPath, $path));
343
344 9
                if ($singleResult) {
345 1
                    return $resources;
346
                }
347 8
            }
348 10
        }
349
350 10
        return $resources;
351
    }
352
353
    /**
354
     * Get all the tree of children under given repository path.
355
     *
356
     * @param string $path The repository path.
357
     *
358
     * @return array
359
     */
360 10
    private function getRecursiveChildren($path)
361
    {
362 10
        $children = array();
363
364
        /*
365
         * Children of a given path either come from real filesystem children
366
         * or from other mappings (virtual resources).
367
         *
368
         * First we check for the real children.
369
         */
370 10
        $filesystemPaths = $this->resolveFilesystemPaths($path, false);
371
372 10
        foreach ($filesystemPaths as $filesystemPath) {
373 9
            $filesystemChildren = $this->getFilesystemPathChildren($path, $filesystemPath, true);
374
375 9
            foreach ($filesystemChildren as $filesystemChild) {
376 5
                $children[$filesystemChild['path']] = $filesystemChild['filesystemPath'];
377 9
            }
378 10
        }
379
380
        /*
381
         * Then we add the children of other path mappings.
382
         * These other path mappings should override possible precedent real children.
383
         */
384 10
        $virtualChildren = $this->getVirtualPathChildren($path, true);
385
386 10
        foreach ($virtualChildren as $virtualChild) {
387 5
            $children[$virtualChild['path']] = $virtualChild['filesystemPath'];
388
389 5
            if ($virtualChild['filesystemPath'] && file_exists($virtualChild['filesystemPath'])) {
390 5
                $filesystemChildren = $this->getFilesystemPathChildren(
391 5
                    $virtualChild['path'],
392 5
                    $virtualChild['filesystemPath'],
393
                    true
394 5
                );
395
396 5
                foreach ($filesystemChildren as $filesystemChild) {
397 3
                    $children[$filesystemChild['path']] = $filesystemChild['filesystemPath'];
398 5
                }
399 5
            }
400 10
        }
401
402 10
        return $children;
403
    }
404
405
    /**
406
     * Get the direct children of the given repository path.
407
     *
408
     * @param string $path         The repository path.
409
     * @param bool   $singleResult Should this method stop after finding a
410
     *                             first result, for performances.
411
     *
412
     * @return ArrayResourceCollection
413
     */
414 11
    private function getDirectChildren($path, $singleResult = false)
415
    {
416 11
        $children = array();
417
418
        /*
419
         * Children of a given path either come from real filesystem children
420
         * or from other mappings (virtual resources).
421
         *
422
         * First we check for the real children.
423
         */
424 11
        $filesystemPaths = $this->resolveFilesystemPaths($path, false);
425
426 11
        foreach ($filesystemPaths as $filesystemPath) {
427 11
            $filesystemChildren = $this->getFilesystemPathChildren($path, $filesystemPath, false);
428
429 11
            foreach ($this->createResources($filesystemChildren) as $child) {
430 9
                if ($singleResult) {
431 3
                    return new ArrayResourceCollection(array($child));
432
                }
433
434 7
                $children[$child->getPath()] = $child;
435 10
            }
436 10
        }
437
438
        /*
439
         * Then we add the children of other path mappings.
440
         * These other path mappings should override possible precedent real children.
441
         */
442 10
        $virtualChildren = $this->createResources($this->getVirtualPathChildren($path, false));
443
444 10
        foreach ($virtualChildren as $child) {
445 4
            if ($singleResult) {
446
                return new ArrayResourceCollection(array($child));
447
            }
448
449 4
            if ($child->getPath() !== $path) {
450 1
                $children[$child->getPath()] = $child;
451 1
            }
452 10
        }
453
454 10
        return new ArrayResourceCollection(array_values($children));
455
    }
456
457
    /**
458
     * Find the children paths of a given filesystem path.
459
     *
460
     * @param string $repositoryPath The repository path
461
     * @param string $filesystemPath The filesystem path
462
     * @param bool   $recursive      Should the method do a recursive listing?
463
     *
464
     * @return array The children paths.
465
     */
466 20
    private function getFilesystemPathChildren($repositoryPath, $filesystemPath, $recursive = false)
467
    {
468 20
        if (!is_dir($filesystemPath)) {
469 7
            return array();
470
        }
471
472 16
        $iterator = new RecursiveDirectoryIterator(
473 16
            $filesystemPath,
474
            FilesystemIterator::KEY_AS_PATHNAME
475 16
            | FilesystemIterator::CURRENT_AS_FILEINFO
476 16
            | FilesystemIterator::SKIP_DOTS
477 16
        );
478
479 16
        if ($recursive) {
480 7
            $iterator = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
481 7
        }
482
483 16
        $childrenFilesystemPaths = array_keys(iterator_to_array($iterator));
484
485
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
486 16
        sort($childrenFilesystemPaths);
487
488 16
        $children = array();
489
490 16
        foreach ($childrenFilesystemPaths as $childFilesystemPath) {
491 16
            $childFilesystemPath = Path::canonicalize($childFilesystemPath);
492
493 16
            $childRepositoryPath = preg_replace(
494 16
                '~^'.preg_quote(rtrim($filesystemPath, '/').'/', '~').'~',
495 16
                rtrim($repositoryPath, '/').'/',
496
                $childFilesystemPath
497 16
            );
498
499 16
            $children[] = array('path' => $childRepositoryPath, 'filesystemPath' => $childFilesystemPath);
500 16
        }
501
502 16
        return $children;
503
    }
504
505
    /**
506
     * Find the children paths of a given virtual path.
507
     *
508
     * @param string $repositoryPath The repository path
509
     * @param bool   $recursive      Should the method do a recursive listing?
510
     *
511
     * @return array The children paths.
512
     */
513 25
    private function getVirtualPathChildren($repositoryPath, $recursive = false)
514
    {
515 25
        $staticPrefix = rtrim($repositoryPath, '/').'/';
516 25
        $regExp = '~^'.preg_quote($staticPrefix, '~');
517
518 25
        if ($recursive) {
519 15
            $regExp .= '.*$~';
520 15
        } else {
521 10
            $regExp .= '[^/]*$~';
522
        }
523
524 25
        $iterator = new RegexFilterIterator(
525 25
            $regExp,
526 25
            $staticPrefix,
527 25
            new ArrayIterator($this->store->keys())
528 25
        );
529
530 25
        $children = array();
531
532 25
        foreach ($iterator as $path) {
533 13
            $filesystemPaths = $this->store->get($path);
534
535 13
            if (!is_array($filesystemPaths)) {
536 5
                $children[] = array('path' => $path, 'filesystemPath' => null);
537 5
                continue;
538
            }
539
540 12
            foreach ($filesystemPaths as $filesystemPath) {
541 12
                $children[] = array('path' => $path, 'filesystemPath' => $this->resolveRelativePath($filesystemPath));
542 12
            }
543 25
        }
544
545 25
        return $children;
546
    }
547
548
    /**
549
     * Create an array of resources using an internal array of children.
550
     *
551
     * @param array $children
552
     *
553
     * @return array
554
     */
555 11
    private function createResources($children)
556
    {
557 11
        $resources = array();
558
559 11
        foreach ($children as $child) {
560 10
            $resources[] = $this->createResource($child['filesystemPath'], $child['path']);
561 11
        }
562
563 11
        return $resources;
564
    }
565
566
    /**
567
     * Check a given path is resolvable.
568
     *
569
     * @param string $path
570
     *
571
     * @return bool
572
     *
573
     * @throws ResourceNotFoundException
574
     */
575 19
    private function isPathResolvable($path)
576
    {
577 19
        return count($this->resolveFilesystemPaths($this->sanitizePath($path))) > 0;
578
    }
579
}
580