Failed Conditions
Pull Request — 1.0 (#64)
by Titouan
04:36
created

OptimizedPathMappingRepository::followLinks()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0188

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 17
ccs 10
cts 11
cp 0.9091
rs 8.8571
cc 5
eloc 9
nc 4
nop 1
crap 5.0188
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 Iterator;
16
use Puli\Repository\Api\EditableRepository;
17
use Puli\Repository\Api\Resource\FilesystemResource;
18
use Puli\Repository\Api\Resource\PuliResource;
19
use Puli\Repository\Api\UnsupportedLanguageException;
20
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
21
use Puli\Repository\Resource\LinkResource;
22
use Webmozart\Assert\Assert;
23
use Webmozart\Glob\Glob;
24
use Webmozart\Glob\Iterator\GlobFilterIterator;
25
use Webmozart\Glob\Iterator\RegexFilterIterator;
26
use Webmozart\PathUtil\Path;
27
28
/**
29
 * An optimized path mapping resource repository.
30
 * When a resource is added, all its children are resolved
31
 * and getting them is much faster.
32
 *
33
 * Resources can be added with the method {@link add()}:
34
 *
35
 * ```php
36
 * use Puli\Repository\OptimizedPathMappingRepository;
37
 *
38
 * $repo = new OptimizedPathMappingRepository();
39
 * $repo->add('/css', new DirectoryResource('/path/to/project/res/css'));
40
 * ```
41
 *
42
 * This repository only supports instances of FilesystemResource.
43
 *
44
 * @since  1.0
45
 *
46
 * @author Bernhard Schussek <[email protected]>
47
 * @author Titouan Galopin <[email protected]>
48
 */
49
class OptimizedPathMappingRepository extends AbstractPathMappingRepository implements EditableRepository
50
{
51
    /**
52
     * {@inheritdoc}
53
     */
54 52
    public function get($path)
55
    {
56 52
        $path = $this->sanitizePath($path);
57
58 43
        if (!$this->store->exists($path)) {
59 6
            return $this->followLinksOrFail($path);
60
        }
61
62 40
        return $this->createResource($this->store->get($path), $path);
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 11
    public function find($query, $language = 'glob')
69
    {
70 11
        $this->validateSearchLanguage($language);
71
72 10
        $query = $this->sanitizePath($query);
73 7
        $resources = new ArrayResourceCollection();
74
75 7
        if (Glob::isDynamic($query)) {
76 5
            $resources = $this->iteratorToCollection($this->getGlobIterator($query));
77 7
        } elseif ($this->store->exists($query)) {
78 2
            $resources = new ArrayResourceCollection(array(
79 2
                $this->createResource($this->store->get($query), $query),
80 2
            ));
81 2
        }
82
83 7
        return $resources;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 19
    public function contains($query, $language = 'glob')
90
    {
91 19
        if ('glob' !== $language) {
92 1
            throw UnsupportedLanguageException::forLanguage($language);
93
        }
94
95 18
        $query = $this->sanitizePath($query);
96
97 15
        if (Glob::isDynamic($query)) {
98 1
            $iterator = $this->getGlobIterator($query);
99 1
            $iterator->rewind();
100
101 1
            return $iterator->valid();
102
        }
103
104 14
        return $this->store->exists($query);
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 48
    public function remove($query, $language = 'glob')
111
    {
112 13
        $this->validateSearchLanguage($language);
113
114 48
        $query = $this->sanitizePath($query);
115
116 9
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
117
118
        // Find resources to remove
119
        // (more efficient that find() as we do not need to unserialize them)
120 7
        $paths = array();
121
122 7
        if (Glob::isDynamic($query)) {
123 2
            $paths = $this->getGlobIterator($query);
124 7
        } elseif ($this->store->exists($query)) {
125 5
            $paths = array($query);
126 5
        }
127
128
        // Remove the resources found
129 7
        $nbOfResources = $this->countStore();
130
131 7
        foreach ($paths as $path) {
132 7
            $this->removePath($path);
133 7
        }
134
135 7
        return $nbOfResources - $this->countStore();
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 11
    public function listChildren($path)
142
    {
143 11
        $iterator = $this->getChildIterator($this->get($path));
144
145 7
        return $this->iteratorToCollection($iterator);
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 7
    public function hasChildren($path)
152
    {
153 7
        $iterator = $this->getChildIterator($this->get($path));
154 3
        $iterator->rewind();
155
156 3
        return $iterator->valid();
157
    }
158
159
    /**
160
     * @param string             $path
161
     * @param FilesystemResource $resource
162
     */
163 70
    protected function addFilesystemResource($path, FilesystemResource $resource)
164
    {
165
        // Read children before attaching the resource to this repository
166 70
        $children = $resource->listChildren();
167
168 70
        $resource->attachTo($this, $path);
169
170
        // Add the resource before adding its children, so that the array stays sorted
171 70
        $this->store->set($path, Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory));
172
173 70
        $basePath = '/' === $path ? $path : $path.'/';
174
175 70
        foreach ($children as $name => $child) {
176 36
            $this->addFilesystemResource($basePath.$name, $child);
177 70
        }
178 70
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 7
    protected function addLinkResource($path, LinkResource $resource)
184
    {
185 7
        $resource->attachTo($this, $path);
186 7
        $this->store->set($path, $this->createLinkTarget($resource->getTargetPath()));
187 7
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 6
    protected function followLinks($path)
193
    {
194 6
        $storePaths = $this->store->getMultiple($this->store->keys());
195
196 6
        foreach ($storePaths as $linkPath => $targetPath) {
197 6
            if ($this->isLinkTarget($targetPath) && 0 === strpos($path, $linkPath)) {
198 3
                $realTargetPath = rtrim($this->getRealTargetPath($storePaths, $linkPath), '/');
199 3
                $realPath = substr_replace($path, $realTargetPath, 0, strlen($linkPath));
200
201 3
                if ($this->store->exists($realPath)) {
202 3
                    return $this->createResource($this->store->get($realPath), $realPath);
203
                }
204
            }
205 6
        }
206
207 3
        return null;
208
    }
209
210
    /**
211
     * Find the real target path of a given link (resolve all the links until
212
     * a non-link resource is found).
213
     *
214
     * @param string[] $storePaths The store paths (to avoid repeated accesses to the store).
215
     * @param string   $linkPath   The link path to fully resolve
216
     *
217
     * @return string
218
     */
219 3
    private function getRealTargetPath($storePaths, $linkPath)
220
    {
221 3
        $targetPath = $storePaths[$linkPath];
222
223 3
        if ($this->isLinkTarget($targetPath)) {
224 3
            return $this->getRealTargetPath($storePaths, $this->getLinkTarget($targetPath));
225
        }
226
227 3
        return $linkPath;
228
    }
229
230
    /**
231
     * @param string $path
232
     */
233 7
    private function removePath($path)
234
    {
235 7
        if (!$this->store->exists($path)) {
236
            return;
237
        }
238
239
        // Remove children first
240 7
        $children = $this->getRecursivePathChildIterator($path);
241
242 7
        foreach ($children as $child) {
243 5
            $this->store->remove($child);
244 7
        }
245
246
        // Remove the resource
247 7
        $this->store->remove($path);
248 7
    }
249
250
    /**
251
     * Returns an iterator for the children paths of a resource.
252
     *
253
     * @param PuliResource $resource The resource.
254
     *
255
     * @return RegexFilterIterator The iterator of paths.
256
     */
257 9 View Code Duplication
    private function getChildIterator(PuliResource $resource)
258
    {
259 9
        $staticPrefix = rtrim($resource->getPath(), '/').'/';
260 9
        $regExp = '~^'.preg_quote($staticPrefix, '~').'[^/]+$~';
261
262 9
        return new RegexFilterIterator(
263 9
            $regExp,
264 9
            $staticPrefix,
265 9
            new ArrayIterator($this->store->keys())
266 9
        );
267
    }
268
269
    /**
270
     * Returns a recursive iterator for the children paths under a given path.
271
     *
272
     * @param string $path The path.
273
     *
274
     * @return RegexFilterIterator The iterator of paths.
275
     */
276 7 View Code Duplication
    private function getRecursivePathChildIterator($path)
277
    {
278 7
        $staticPrefix = rtrim($path, '/').'/';
279 7
        $regExp = '~^'.preg_quote($staticPrefix, '~').'.+$~';
280
281 7
        return new RegexFilterIterator(
282 7
            $regExp,
283 7
            $staticPrefix,
284 7
            new ArrayIterator($this->store->keys())
285 7
        );
286
    }
287
288
    /**
289
     * Returns an iterator for a glob.
290
     *
291
     * @param string $glob The glob.
292
     *
293
     * @return GlobFilterIterator The iterator of paths.
294
     */
295 8
    private function getGlobIterator($glob)
296
    {
297 8
        return new GlobFilterIterator(
298 8
            $glob,
299 8
            new ArrayIterator($this->store->keys())
300 8
        );
301
    }
302
303
    /**
304
     * Transform an iterator of paths into a collection of resources.
305
     *
306
     * @param Iterator $iterator
307
     *
308
     * @return ArrayResourceCollection
309
     */
310 12
    private function iteratorToCollection(Iterator $iterator)
311
    {
312 12
        $filesystemPaths = $this->store->getMultiple(iterator_to_array($iterator));
313 12
        $collection = new ArrayResourceCollection();
314
315 12
        foreach ($filesystemPaths as $path => $filesystemPath) {
316 9
            $collection->add($this->createResource($filesystemPath, $path));
317 12
        }
318
319 12
        return $collection;
320
    }
321
}
322