Completed
Pull Request — 1.0 (#91)
by Titouan
21:47 queued 19:07
created

FilesystemRepository   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 545
Duplicated Lines 5.5 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 95.83%

Importance

Changes 13
Bugs 3 Features 4
Metric Value
wmc 81
c 13
b 3
f 4
lcom 2
cbo 15
dl 30
loc 545
ccs 207
cts 216
cp 0.9583
rs 1.5789

24 Methods

Rating   Name   Duplication   Size   Complexity  
A find() 0 4 1
A isSymlinkSupported() 0 15 3
A __construct() 0 13 3
A get() 11 11 2
A contains() 0 7 1
A hasChildren() 0 17 3
A listChildren() 0 14 3
B add() 8 25 5
A remove() 0 14 2
A clear() 0 16 2
A ensureDirectoryExists() 0 16 3
C addResource() 0 74 14
B removeResource() 0 22 5
B createResource() 0 26 6
A iteratorToCollection() 0 20 3
A getFilesystemPath() 11 11 2
A getGlobIterator() 0 11 1
A getDirectoryIterator() 0 7 1
C symlinkMirror() 0 44 11
B replaceParentSymlinksByCopies() 0 35 3
A replaceLinkByCopy() 0 7 1
A trySymlink() 0 13 3
A readLink() 0 18 2
A getPath() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FilesystemRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FilesystemRepository, and based on these observations, apply Extract Interface, too.

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 Iterator;
15
use Puli\Repository\Api\ChangeStream\ChangeStream;
16
use Puli\Repository\Api\Resource\BodyResource;
17
use Puli\Repository\Api\Resource\FilesystemResource;
18
use Puli\Repository\Api\Resource\PuliResource;
19
use Puli\Repository\Api\ResourceCollection;
20
use Puli\Repository\Api\ResourceNotFoundException;
21
use Puli\Repository\Api\UnsupportedOperationException;
22
use Puli\Repository\Api\UnsupportedResourceException;
23
use Puli\Repository\Resource\Collection\FilesystemResourceCollection;
24
use Puli\Repository\Resource\DirectoryResource;
25
use Puli\Repository\Resource\FileResource;
26
use Puli\Repository\Resource\LinkResource;
27
use Symfony\Component\Filesystem\Exception\IOException;
28
use Symfony\Component\Filesystem\Filesystem;
29
use Webmozart\Assert\Assert;
30
use Webmozart\Glob\Iterator\GlobIterator;
31
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
32
use Webmozart\PathUtil\Path;
33
34
/**
35
 * A repository reading from the file system.
36
 *
37
 * Resources can be read using their absolute file system paths:
38
 *
39
 * ```php
40
 * use Puli\Repository\FilesystemRepository;
41
 *
42
 * $repo = new FilesystemRepository();
43
 * $resource = $repo->get('/home/puli/.gitconfig');
44
 * ```
45
 *
46
 * The returned resources implement {@link FilesystemResource}.
47
 *
48
 * Optionally, a root directory can be passed to the constructor. Then all paths
49
 * will be read relative to that directory:
50
 *
51
 * ```php
52
 * $repo = new FilesystemRepository('/home/puli');
53
 * $resource = $repo->get('/.gitconfig');
54
 * ```
55
 *
56
 * While "." and ".." segments are supported, files outside the root directory
57
 * cannot be read. Any leading ".." segments will simply be stripped off.
58
 *
59
 * @since  1.0
60
 *
61
 * @author Bernhard Schussek <[email protected]>
62
 */
63
class FilesystemRepository extends AbstractEditableRepository
64
{
65
    /**
66
     * @var bool|null
67
     */
68
    private static $symlinkSupported;
69
70
    /**
71
     * @var string
72
     */
73
    private $baseDir;
74
75
    /**
76
     * @var int
77
     */
78
    private $baseDirLength;
79
80
    /**
81
     * @var bool
82
     */
83
    private $symlink;
84
85
    /**
86
     * @var bool
87
     */
88
    private $relative;
89
90
    /**
91
     * @var Filesystem
92
     */
93
    private $filesystem;
94
95
    /**
96
     * Returns whether symlinks are supported in the local environment.
97
     *
98
     * @return bool Returns `true` if symlinks are supported.
99
     */
100 305
    public static function isSymlinkSupported()
101
    {
102 305
        if (null === self::$symlinkSupported) {
103
            // http://php.net/manual/en/function.symlink.php
104
            // Symlinks are only supported on Windows Vista, Server 2008 or
105
            // greater on PHP 5.3+
106 1
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
107
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
108
            } else {
109 1
                self::$symlinkSupported = true;
110
            }
111
        }
112
113 305
        return self::$symlinkSupported;
114
    }
115
116
    /**
117
     * Creates a new repository.
118
     *
119
     * @param string            $baseDir      The base directory of the repository on the file
120
     *                                        system.
121
     * @param bool              $symlink      Whether to use symbolic links for added files. If
122
     *                                        symbolic links are not supported on the current
123
     *                                        system, the repository will create hard copies
124
     *                                        instead.
125
     * @param bool              $relative     Whether to create relative symbolic links. If
126
     *                                        relative links are not supported on the current
127
     *                                        system, the repository will create absolute links
128
     *                                        instead.
129
     * @param ChangeStream|null $changeStream If provided, the repository will log
130
     *                                        resources changes in this change stream.
131
     */
132 394
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
133
    {
134 394
        parent::__construct($changeStream);
135
136 394
        Assert::directory($baseDir);
137 394
        Assert::boolean($symlink);
138
139 394
        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
140 394
        $this->baseDirLength = strlen($baseDir);
141 394
        $this->symlink = $symlink && self::isSymlinkSupported();
142 394
        $this->relative = $this->symlink && $relative;
143 394
        $this->filesystem = new Filesystem();
144 394
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 169 View Code Duplication
    public function get($path)
150
    {
151 169
        $path = $this->sanitizePath($path);
152 157
        $filesystemPath = $this->baseDir.$path;
153
154 157
        if (!file_exists($filesystemPath)) {
155 8
            throw ResourceNotFoundException::forPath($path);
156
        }
157
158 149
        return $this->createResource($filesystemPath, $path);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 41
    public function find($query, $language = 'glob')
165
    {
166 41
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 63
    public function contains($query, $language = 'glob')
173
    {
174 63
        $iterator = $this->getGlobIterator($query, $language);
175 50
        $iterator->rewind();
176
177 50
        return $iterator->valid();
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 28
    public function hasChildren($path)
184
    {
185 28
        $filesystemPath = $this->getFilesystemPath($path);
186
187 12
        if (is_link($filesystemPath)) {
188
            $filesystemPath = $this->readLink($filesystemPath);
189
        }
190
191 12
        if (!is_dir($filesystemPath)) {
192 4
            return false;
193
        }
194
195 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
196 12
        $iterator->rewind();
197
198 12
        return $iterator->valid();
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 42
    public function listChildren($path)
205
    {
206 42
        $filesystemPath = $this->getFilesystemPath($path);
207
208 26
        if (is_link($filesystemPath)) {
209 2
            $filesystemPath = $this->readLink($filesystemPath);
210
        }
211
212 26
        if (!is_dir($filesystemPath)) {
213 4
            return new FilesystemResourceCollection();
214
        }
215
216 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 359
    public function add($path, $resource)
223
    {
224 359
        $path = $this->sanitizePath($path);
225
226 347 View Code Duplication
        if ($resource instanceof ResourceCollection) {
227 4
            $this->ensureDirectoryExists($path);
228 4
            foreach ($resource as $child) {
229 4
                $this->addResource($path.'/'.$child->getName(), $child);
230
            }
231
232 4
            return;
233
        }
234
235 343
        if ($resource instanceof PuliResource) {
236 339
            $this->ensureDirectoryExists(Path::getDirectory($path));
237 339
            $this->addResource($path, $resource);
238
239 334
            return;
240
        }
241
242 4
        throw new UnsupportedResourceException(sprintf(
243 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
244 4
            is_object($resource) ? get_class($resource) : gettype($resource)
245
        ));
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 61
    public function remove($query, $language = 'glob')
252
    {
253 61
        $iterator = $this->getGlobIterator($query, $language);
254 48
        $removed = 0;
255
256 48
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
257
258
        // There's some problem with concurrent deletions at the moment
259 40
        foreach (iterator_to_array($iterator) as $filesystemPath) {
260 40
            $this->removeResource($filesystemPath, $removed);
261
        }
262
263 40
        return $removed;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269 14
    public function clear()
270
    {
271 14
        $iterator = $this->getDirectoryIterator($this->baseDir);
272 14
        $removed = 0;
273
274
        // Batch-delete all versions
275 14
        $this->clearVersions();
276
277 14
        foreach ($iterator as $filesystemPath) {
278 10
            $this->removeResource($filesystemPath, $removed);
279
        }
280
281 14
        $this->storeVersion($this->get('/'));
282
283 14
        return $removed;
284
    }
285
286 343
    private function ensureDirectoryExists($path)
287
    {
288 343
        $filesystemPath = $this->baseDir.$path;
289
290 343
        if (is_file($filesystemPath)) {
291 1
            throw new UnsupportedOperationException(sprintf(
292
                'Instances of BodyResource do not support child resources in '.
293 1
                'FilesystemRepository. Tried to add a child to %s.',
294
                $filesystemPath
295
            ));
296
        }
297
298 343
        if (!is_dir($filesystemPath)) {
299 112
            mkdir($filesystemPath, 0777, true);
300
        }
301 343
    }
302
303 343
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
304
    {
305 343
        $pathInBaseDir = $this->baseDir.$path;
306 343
        $hasChildren = $resource->hasChildren();
307 343
        $hasBody = $resource instanceof BodyResource;
308
309 343
        if ($hasChildren && $hasBody) {
310 1
            throw new UnsupportedResourceException(sprintf(
311
                'Instances of BodyResource do not support child resources in '.
312
                'FilesystemRepository. Tried to add a BodyResource with '.
313 1
                'children at %s.',
314
                $path
315
            ));
316
        }
317
318 342
        $resource = clone $resource;
319 342
        $resource->attachTo($this, $path);
320
321 342
        if ($this->symlink && $checkParentsForSymlinks) {
322 184
            $this->replaceParentSymlinksByCopies($path);
323
        }
324
325 342
        if ($resource instanceof FilesystemResource) {
326 35
            if ($this->symlink) {
327 32
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
328 3
            } elseif ($hasBody) {
329 2
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
330
            } else {
331 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
332
            }
333
334 35
            $this->storeVersion($resource);
335
336 35
            return;
337
        }
338
339 315
        if ($resource instanceof LinkResource) {
340 8
            if (!$this->symlink) {
341 4
                throw new UnsupportedResourceException(sprintf(
342
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
343 4
                    'Tried to add a LinkResource at %s.',
344
                    $path
345
                ));
346
            }
347
348 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
349
350 4
            $this->storeVersion($resource);
351
352 4
            return;
353
        }
354
355 307
        if ($hasBody) {
356 175
            file_put_contents($pathInBaseDir, $resource->getBody());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Puli\Repository\Api\Resource\PuliResource as the method getBody() does only exist in the following implementations of said interface: Puli\Repository\Resource\FileResource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
357
358 175
            $this->storeVersion($resource);
359
360 175
            return;
361
        }
362
363 239
        if (is_file($pathInBaseDir)) {
364
            $this->filesystem->remove($pathInBaseDir);
365
        }
366
367 239
        if (!file_exists($pathInBaseDir)) {
368 147
            mkdir($pathInBaseDir, 0777, true);
369
        }
370
371 239
        foreach ($resource->listChildren() as $child) {
372 148
            $this->addResource($path.'/'.$child->getName(), $child, false);
373
        }
374
375 239
        $this->storeVersion($resource);
376 239
    }
377
378 50
    private function removeResource($filesystemPath, &$removed)
379
    {
380
        // Skip paths that have already been removed
381 50
        if (!file_exists($filesystemPath)) {
382
            return;
383
        }
384
385 50
        $this->removeVersions($this->getPath($filesystemPath));
386
387 50
        ++$removed;
388
389 50
        if (is_dir($filesystemPath) && !is_link($filesystemPath)) {
390 32
            $iterator = $this->getDirectoryIterator($filesystemPath);
391
392 32
            foreach ($iterator as $childFilesystemPath) {
393
                // Remove children and child versions
394 32
                $this->removeResource($childFilesystemPath, $removed);
395
            }
396
        }
397
398 50
        $this->filesystem->remove($filesystemPath);
399 50
    }
400
401 149
    private function createResource($filesystemPath, $path)
402
    {
403 149
        $resource = null;
404
405 149
        if (is_link($filesystemPath)) {
406 6
            $baseDir = rtrim($this->baseDir, '/');
407 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
408
409 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
410 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
411 6
                $resource = new LinkResource($targetPath);
412
            }
413
        }
414
415 149
        if (!$resource && is_dir($filesystemPath)) {
416 100
            $resource = new DirectoryResource($filesystemPath);
417
        }
418
419 149
        if (!$resource) {
420 79
            $resource = new FileResource($filesystemPath);
421
        }
422
423 149
        $resource->attachTo($this, $path);
424
425 149
        return $resource;
426
    }
427
428 50
    private function iteratorToCollection(Iterator $iterator)
429
    {
430 50
        $filesystemPaths = iterator_to_array($iterator);
431 50
        $resources = array();
432
433
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
434 50
        sort($filesystemPaths);
435
436 50
        foreach ($filesystemPaths as $filesystemPath) {
437 42
            $resource = is_dir($filesystemPath)
438 24
                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))
439 42
                : new FileResource($filesystemPath, $this->getPath($filesystemPath));
440
441 42
            $resource->attachTo($this);
442
443 42
            $resources[] = $resource;
444
        }
445
446 50
        return new FilesystemResourceCollection($resources);
447
    }
448
449 66 View Code Duplication
    private function getFilesystemPath($path)
450
    {
451 66
        $path = $this->sanitizePath($path);
452 42
        $filesystemPath = $this->baseDir.$path;
453
454 42
        if (!file_exists($filesystemPath)) {
455 8
            throw ResourceNotFoundException::forPath($path);
456
        }
457
458 34
        return $filesystemPath;
459
    }
460
461 137
    private function getGlobIterator($query, $language)
462
    {
463 137
        $this->failUnlessGlob($language);
464
465 134
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
466 110
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
467
468 98
        $query = Path::canonicalize($query);
469
470 98
        return new GlobIterator($this->baseDir.$query);
471
    }
472
473 84
    private function getDirectoryIterator($filesystemPath)
474
    {
475 84
        return new RecursiveDirectoryIterator(
476
            $filesystemPath,
477 84
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
478
        );
479
    }
480
481 32
    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
482
    {
483 32
        $targetIsDir = is_dir($target);
484 32
        $forceDir = in_array($target, $dirsToKeep, true);
485
486
        // Merge directories
487 32
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
488 16
            if (is_link($target)) {
489 4
                $this->replaceLinkByCopy($target, $dirsToKeep);
490
            }
491
492 16
            $iterator = $this->getDirectoryIterator($origin);
493
494 16
            foreach ($iterator as $path) {
495 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
496
            }
497
498 16
            return;
499
        }
500
501
        // Replace target
502 32
        if (file_exists($target)) {
503 10
            $this->filesystem->remove($target);
504
        }
505
506
        // Try creating a relative link
507 32
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
508 16
            return;
509
        }
510
511
        // Try creating a absolute link
512 16
        if ($this->trySymlink($origin, $target)) {
513 16
            return;
514
        }
515
516
        // Fall back to copy
517
        if (is_dir($origin)) {
518
            $this->filesystem->mirror($origin, $target);
519
520
            return;
521
        }
522
523
        $this->filesystem->copy($origin, $target);
524
    }
525
526 184
    private function replaceParentSymlinksByCopies($path)
527
    {
528 184
        $previousPath = null;
529
530
        // Collect all paths that MUST NOT be symlinks after doing the
531
        // replace operation.
532
        //
533
        // Example:
534
        //
535
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
536
        //
537
        // Before:
538
        //   /webmozart -> target
539
        //
540
        // After:
541
        //   /webmozart
542
        //     /config -> target/config
543
        //     /views
544
        //       /index.html.twig -> target/views/index.html.twig
545
546 184
        $dirsToKeep = array();
547
548 184
        while ($previousPath !== ($path = Path::getDirectory($path))) {
549 184
            $filesystemPath = $this->baseDir.$path;
550 184
            $dirsToKeep[] = $filesystemPath;
551
552 184
            if (is_link($filesystemPath)) {
553 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
554
555 12
                return;
556
            }
557
558 184
            $previousPath = $path;
559
        }
560 184
    }
561
562 16
    private function replaceLinkByCopy($path, array $dirsToKeep = array())
563
    {
564 16
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
565 16
        $this->filesystem->remove($path);
566 16
        $this->filesystem->mkdir($path);
567 16
        $this->symlinkMirror($target, $path, $dirsToKeep);
568 16
    }
569
570 32
    private function trySymlink($origin, $target)
571
    {
572
        try {
573 32
            $this->filesystem->symlink($origin, $target);
574
575 32
            if (file_exists($target)) {
576 32
                return true;
577
            }
578
        } catch (IOException $e) {
579
        }
580
581
        return false;
582
    }
583
584 22
    private function readLink($filesystemPath)
585
    {
586
        // On Windows, transitive links are resolved to the final target by
587
        // readlink(). realpath(), however, returns the target link on Windows,
588
        // but not on Unix.
589
590
        // /link1 -> /link2 -> /file
591
592
        // Windows: readlink(/link1) => /file
593
        //          realpath(/link1) => /link2
594
595
        // Unix:    readlink(/link1) => /link2
596
        //          realpath(/link1) => /file
597
598
        // Consistency FTW!
599
600 22
        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
601
    }
602
603 92
    private function getPath($filesystemPath)
604
    {
605 92
        return substr($filesystemPath, $this->baseDirLength);
606
    }
607
}
608