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

OptimizedPathMappingRepository::listChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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