Failed Conditions
Pull Request — 1.0 (#64)
by Titouan
05:16
created

PathMappingRepository::find()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 4
Bugs 1 Features 1
Metric Value
c 4
b 1
f 1
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4286
cc 1
eloc 4
nc 1
nop 2
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\ResourceNotFoundException;
20
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
21
use Puli\Repository\Resource\LinkResource;
22
use RecursiveDirectoryIterator;
23
use RecursiveIteratorIterator;
24
use Webmozart\Assert\Assert;
25
use Webmozart\Glob\Glob;
26
use Webmozart\Glob\Iterator\RegexFilterIterator;
27
use Webmozart\PathUtil\Path;
28
29
/**
30
 * A development path mapping resource repository.
31
 * Each resource is resolved at `get()` time to improve
32
 * developer experience.
33
 *
34
 * Resources can be added with the method {@link add()}:
35
 *
36
 * ```php
37
 * use Puli\Repository\PathMappingRepository;
38
 *
39
 * $repo = new PathMappingRepository();
40
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
41
 * ```
42
 *
43
 * This repository only supports instances of FilesystemResource.
44
 *
45
 * @since  1.0
46
 *
47
 * @author Bernhard Schussek <[email protected]>
48
 * @author Titouan Galopin <[email protected]>
49
 */
50
class PathMappingRepository extends AbstractPathMappingRepository implements EditableRepository
51
{
52
    /**
53
     * {@inheritdoc}
54
     */
55 40
    public function get($path)
56
    {
57 40
        $path = $this->sanitizePath($path);
58 37
        $filesystemPaths = $this->resolveFilesystemPaths($path);
59
60 37
        if (0 === count($filesystemPaths)) {
61 2
            return $this->resolveWithLinksOrFail($path);
62
        }
63
64 36
        return $this->createResource(reset($filesystemPaths), $path);
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 20
    public function find($query, $language = 'glob')
71
    {
72 20
        $this->validateSearchLanguage($language);
73 19
        $query = $this->sanitizePath($query);
74
75 16
        return $this->search($query);
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 21
    public function contains($query, $language = 'glob')
82
    {
83 21
        $this->validateSearchLanguage($language);
84 20
        $query = $this->sanitizePath($query);
85
86 17
        return !$this->search($query, true)->isEmpty();
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 12
    public function remove($query, $language = 'glob')
93
    {
94 12
        $query = $this->sanitizePath($query);
95
96 9
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
97
98 7
        $results = $this->find($query, $language);
99 7
        $invalid = array();
100
101 7
        foreach ($results as $result) {
102 7
            if (!$this->store->exists($result->getPath())) {
103
                $invalid[] = $result->getFilesystemPath();
104
            }
105 7
        }
106
107 7
        if (count($invalid) === 1) {
108
            throw new BadMethodCallException(sprintf(
109
                'The remove query "%s" matched a resource that is not a path mapping', $query
110
            ));
111 7
        } elseif (count($invalid) > 1) {
112
            throw new BadMethodCallException(sprintf(
113
                'The remove query "%s" matched %s resources that are not path mappings', $query, count($invalid)
114
            ));
115
        }
116
117 7
        $removed = 0;
118
119 7
        foreach ($results as $result) {
120 7
            foreach ($this->getVirtualPathChildren($result->getPath(), true) as $virtualChild) {
121 5
                if ($this->store->remove($virtualChild['path'])) {
122 5
                    ++$removed;
123 5
                }
124 7
            }
125
126 7
            if ($this->store->remove($result->getPath())) {
127 7
                ++$removed;
128 7
            }
129 7
        }
130
131 7
        return $removed;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 13
    public function listChildren($path)
138
    {
139 13
        if (!$this->isPathResolvable($path)) {
140 1
            throw ResourceNotFoundException::forPath($path);
141
        }
142
143 9
        return $this->getDirectChildren($this->sanitizePath($path));
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 7
    public function hasChildren($path)
150
    {
151 7
        if (!$this->isPathResolvable($path)) {
152 1
            throw ResourceNotFoundException::forPath($path);
153
        }
154
155 3
        return !$this->getDirectChildren($this->sanitizePath($path), true)->isEmpty();
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 70
    protected function addFilesystemResource($path, FilesystemResource $resource)
162
    {
163 70
        $resource->attachTo($this, $path);
164 70
        $this->storeUnshift($path, Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory));
165 70
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 3
    protected function addLinkResource($path, LinkResource $resource)
171
    {
172 3
        $resource->attachTo($this, $path);
173 3
        $this->storeUnshift($path, 'l:'.$resource->getTargetPath());
174 3
    }
175
176
    /**
177
     * Add a target path (link or filesystem path) to the beginning of the stack in the store at a path.
178
     *
179
     * @param string $path
180
     * @param string $targetPath
181
     */
182 70
    private function storeUnshift($path, $targetPath)
183
    {
184 70
        $previousPaths = array();
185
186 70
        if ($this->store->exists($path)) {
187 48
            $previousPaths = (array) $this->store->get($path);
188 48
        }
189
190 70
        if (!in_array($targetPath, $previousPaths, true)) {
191 70
            array_unshift($previousPaths, $targetPath);
192 70
        }
193
194 70
        $this->store->set($path, $previousPaths);
195 70
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 2
    protected function resolveWithLinks($path)
201
    {
202 2
        $store = $this->store->getMultiple($this->store->keys());
203
204 2
        foreach ($store as $resourcePath => $filesystemPaths) {
205 2
            if (!is_array($filesystemPaths)) {
206 1
                continue;
207
            }
208
209 2
            if (0 !== strpos($path, $resourcePath)) {
210 1
                continue;
211
            }
212
213 2
            foreach ($filesystemPaths as $filesystemPath) {
214 2
                if (0 === strpos($filesystemPath, 'l:')) {
215 1
                    $targetPath = substr($filesystemPath, 2);
216 1
                    $realpath = substr_replace($path, rtrim($targetPath, '/'), 0, strlen($resourcePath));
217 1
                    $resolved = $this->resolveFilesystemPaths($realpath);
218
219 1
                    if (count($resolved) > 0) {
220 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...
221
                    }
222 1
                }
223 2
            }
224 2
        }
225
226 2
        return null;
227
    }
228
229
    /**
230
     * Return the filesystem path associated to the given repository path
231
     * or null if no filesystem path is found.
232
     *
233
     * @param string $path      The repository path.
234
     * @param bool   $onlyFirst Should the method stop on the first path found?
235
     *
236
     * @return string[]|null[]
237
     */
238 58
    private function resolveFilesystemPaths($path, $onlyFirst = true)
239
    {
240
        /*
241
         * If the path exists in the store, return it directly
242
         */
243 58
        if ($this->store->exists($path)) {
244 36
            $filesystemPaths = $this->store->get($path);
245
246 36
            if (is_array($filesystemPaths) && count($filesystemPaths) > 0) {
247 33
                if ($onlyFirst) {
248 32
                    return $this->resolveRelativePaths(array(reset($filesystemPaths)));
249
                }
250
251 7
                return $this->resolveRelativePaths($filesystemPaths);
252
            }
253
254 12
            return array(null);
255
        }
256
257
        /*
258
         * Otherwise, we need to "resolve" it in two steps:
259
         *      1.  find the resources from the store that are potential parents
260
         *          of the path (we filter them using Path::isBasePath)
261
         *      2.  for each of these potential parent, we try to find a real
262
         *          file or directory on the filesystem and if we do find one,
263
         *          we stop
264
         */
265 41
        $basePaths = array_reverse($this->store->keys());
266 41
        $filesystemPaths = array();
267
268 41
        foreach ($basePaths as $key => $basePath) {
269 41
            if (!Path::isBasePath($basePath, $path)) {
270 3
                continue;
271
            }
272
273 41
            $filesystemBasePaths = $this->resolveRelativePaths((array) $this->store->get($basePath));
274 41
            $basePathLength = strlen(rtrim($basePath, '/').'/');
275
276 41
            foreach ($filesystemBasePaths as $filesystemBasePath) {
277 33
                $filesystemBasePath = rtrim($filesystemBasePath, '/').'/';
278 33
                $filesystemPath = substr_replace($path, $filesystemBasePath, 0, $basePathLength);
279
280
                // File
281 33
                if (file_exists($filesystemPath)) {
282 29
                    $filesystemPaths[] = $filesystemPath;
283
284 29
                    if ($onlyFirst) {
285 28
                        return $this->resolveRelativePaths($filesystemPaths);
286
                    }
287 11
                }
288 27
            }
289 26
        }
290
291 26
        return $this->resolveRelativePaths($filesystemPaths);
292
    }
293
294
    /**
295
     * Search for resources by querying their path.
296
     *
297
     * @param string $query        The glob query.
298
     * @param bool   $singleResult Should this method stop after finding a
299
     *                             first result, for performances.
300
     *
301
     * @return ArrayResourceCollection The results of search.
302
     */
303 26
    private function search($query, $singleResult = false)
304
    {
305 26
        $resources = new ArrayResourceCollection();
306
307
        // If the query is not a glob, return it directly
308 26
        if (!Glob::isDynamic($query)) {
309 18
            $filesystemPaths = $this->resolveFilesystemPaths($query);
310
311 18
            if (count($filesystemPaths) > 0) {
312 18
                $resources->add($this->createResource(reset($filesystemPaths), $query));
313 18
            }
314
315 18
            return $resources;
316
        }
317
318
        // If the glob is dynamic, we search
319 10
        $children = $this->getRecursiveChildren(Glob::getBasePath($query));
320
321 10
        foreach ($children as $path => $filesystemPath) {
322 9
            if (Glob::match($path, $query)) {
323 9
                $resources->add($this->createResource($filesystemPath, $path));
324
325 9
                if ($singleResult) {
326 1
                    return $resources;
327
                }
328 8
            }
329 10
        }
330
331 10
        return $resources;
332
    }
333
334
    /**
335
     * Get all the tree of children under given repository path.
336
     *
337
     * @param string $path The repository path.
338
     *
339
     * @return array
340
     */
341 10
    private function getRecursiveChildren($path)
342
    {
343 10
        $children = array();
344
345
        /*
346
         * Children of a given path either come from real filesystem children
347
         * or from other mappings (virtual resources).
348
         *
349
         * First we check for the real children.
350
         */
351 10
        $filesystemPaths = $this->resolveFilesystemPaths($path, false);
352
353 10
        foreach ($filesystemPaths as $filesystemPath) {
354 9
            $filesystemChildren = $this->getFilesystemPathChildren($path, $filesystemPath, true);
355
356 9
            foreach ($filesystemChildren as $filesystemChild) {
357 5
                $children[$filesystemChild['path']] = $filesystemChild['filesystemPath'];
358 9
            }
359 10
        }
360
361
        /*
362
         * Then we add the children of other path mappings.
363
         * These other path mappings should override possible precedent real children.
364
         */
365 10
        $virtualChildren = $this->getVirtualPathChildren($path, true);
366
367 10
        foreach ($virtualChildren as $virtualChild) {
368 5
            $children[$virtualChild['path']] = $virtualChild['filesystemPath'];
369
370 5
            if ($virtualChild['filesystemPath'] && file_exists($virtualChild['filesystemPath'])) {
371 5
                $filesystemChildren = $this->getFilesystemPathChildren(
372 5
                    $virtualChild['path'],
373 5
                    $virtualChild['filesystemPath'],
374
                    true
375 5
                );
376
377 5
                foreach ($filesystemChildren as $filesystemChild) {
378 3
                    $children[$filesystemChild['path']] = $filesystemChild['filesystemPath'];
379 5
                }
380 5
            }
381 10
        }
382
383 10
        return $children;
384
    }
385
386
    /**
387
     * Get the direct children of the given repository path.
388
     *
389
     * @param string $path         The repository path.
390
     * @param bool   $singleResult Should this method stop after finding a
391
     *                             first result, for performances.
392
     *
393
     * @return ArrayResourceCollection
394
     */
395 11
    private function getDirectChildren($path, $singleResult = false)
396
    {
397 11
        $children = array();
398
399
        /*
400
         * Children of a given path either come from real filesystem children
401
         * or from other mappings (virtual resources).
402
         *
403
         * First we check for the real children.
404
         */
405 11
        $filesystemPaths = $this->resolveFilesystemPaths($path, false);
406
407 11
        foreach ($filesystemPaths as $filesystemPath) {
408 11
            $filesystemChildren = $this->getFilesystemPathChildren($path, $filesystemPath, false);
409
410 11
            foreach ($this->createResources($filesystemChildren) as $child) {
411 9
                if ($singleResult) {
412 3
                    return new ArrayResourceCollection(array($child));
413
                }
414
415 7
                $children[$child->getPath()] = $child;
416 10
            }
417 10
        }
418
419
        /*
420
         * Then we add the children of other path mappings.
421
         * These other path mappings should override possible precedent real children.
422
         */
423 10
        $virtualChildren = $this->createResources($this->getVirtualPathChildren($path, false));
424
425 10
        foreach ($virtualChildren as $child) {
426 4
            if ($singleResult) {
427
                return new ArrayResourceCollection(array($child));
428
            }
429
430 4
            if ($child->getPath() !== $path) {
431 1
                $children[$child->getPath()] = $child;
432 1
            }
433 10
        }
434
435 10
        return new ArrayResourceCollection(array_values($children));
436
    }
437
438
    /**
439
     * Find the children paths of a given filesystem path.
440
     *
441
     * @param string $repositoryPath The repository path
442
     * @param string $filesystemPath The filesystem path
443
     * @param bool   $recursive      Should the method do a recursive listing?
444
     *
445
     * @return array The children paths.
446
     */
447 20
    private function getFilesystemPathChildren($repositoryPath, $filesystemPath, $recursive = false)
448
    {
449 20
        if (!is_dir($filesystemPath)) {
450 7
            return array();
451
        }
452
453 16
        $iterator = new RecursiveDirectoryIterator(
454 16
            $filesystemPath,
455
            FilesystemIterator::KEY_AS_PATHNAME
456 16
            | FilesystemIterator::CURRENT_AS_FILEINFO
457 16
            | FilesystemIterator::SKIP_DOTS
458 16
        );
459
460 16
        if ($recursive) {
461 7
            $iterator = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
462 7
        }
463
464 16
        $childrenFilesystemPaths = array_keys(iterator_to_array($iterator));
465
466
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
467 16
        sort($childrenFilesystemPaths);
468
469 16
        $children = array();
470
471 16
        foreach ($childrenFilesystemPaths as $childFilesystemPath) {
472 16
            $childFilesystemPath = Path::canonicalize($childFilesystemPath);
473
474 16
            $childRepositoryPath = preg_replace(
475 16
                '~^'.preg_quote(rtrim($filesystemPath, '/').'/', '~').'~',
476 16
                rtrim($repositoryPath, '/').'/',
477
                $childFilesystemPath
478 16
            );
479
480 16
            $children[] = array('path' => $childRepositoryPath, 'filesystemPath' => $childFilesystemPath);
481 16
        }
482
483 16
        return $children;
484
    }
485
486
    /**
487
     * Find the children paths of a given virtual path.
488
     *
489
     * @param string $repositoryPath The repository path
490
     * @param bool   $recursive      Should the method do a recursive listing?
491
     *
492
     * @return array The children paths.
493
     */
494 25
    private function getVirtualPathChildren($repositoryPath, $recursive = false)
495
    {
496 25
        $staticPrefix = rtrim($repositoryPath, '/').'/';
497 25
        $regExp = '~^'.preg_quote($staticPrefix, '~');
498
499 25
        if ($recursive) {
500 15
            $regExp .= '.*$~';
501 15
        } else {
502 10
            $regExp .= '[^/]*$~';
503
        }
504
505 25
        $iterator = new RegexFilterIterator(
506 25
            $regExp,
507 25
            $staticPrefix,
508 25
            new ArrayIterator($this->store->keys())
509 25
        );
510
511 25
        $children = array();
512
513 25
        foreach ($iterator as $path) {
514 13
            $filesystemPaths = $this->store->get($path);
515
516 13
            if (!is_array($filesystemPaths)) {
517 5
                $children[] = array('path' => $path, 'filesystemPath' => null);
518 5
                continue;
519
            }
520
521 12
            foreach ($filesystemPaths as $filesystemPath) {
522 12
                $children[] = array('path' => $path, 'filesystemPath' => $this->resolveRelativePath($filesystemPath));
523 12
            }
524 25
        }
525
526 25
        return $children;
527
    }
528
529
    /**
530
     * Create an array of resources using an internal array of children.
531
     *
532
     * @param array $children
533
     *
534
     * @return array
535
     */
536 11
    private function createResources($children)
537
    {
538 11
        $resources = array();
539
540 11
        foreach ($children as $child) {
541 10
            $resources[] = $this->createResource($child['filesystemPath'], $child['path']);
542 11
        }
543
544 11
        return $resources;
545
    }
546
547
    /**
548
     * Check a given path is resolvable.
549
     *
550
     * @param string $path
551
     *
552
     * @return bool
553
     *
554
     * @throws ResourceNotFoundException
555
     */
556 19
    private function isPathResolvable($path)
557
    {
558 19
        return count($this->resolveFilesystemPaths($this->sanitizePath($path))) > 0;
559
    }
560
}
561