Failed Conditions
Pull Request — 1.0 (#64)
by Titouan
03:11
created

PathMappingRepository::followLinksInternal()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 8.008

Importance

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