Completed
Push — 1.0 ( ca57a4...6b0f55 )
by Bernhard
02:48
created

PathMappingRepository::getRecursiveChildren()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 44
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7

Importance

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