FilesystemRepository   F
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 537
Duplicated Lines 5.59 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 95.77%

Importance

Changes 12
Bugs 2 Features 4
Metric Value
wmc 79
c 12
b 2
f 4
lcom 2
cbo 15
dl 30
loc 537
ccs 204
cts 213
cp 0.9577
rs 2.0547

24 Methods

Rating   Name   Duplication   Size   Complexity  
A find() 0 4 1
A getPath() 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 13 2
A listChildren() 0 10 2
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 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
B removeResource() 0 22 5

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_dir($filesystemPath)) {
188 4
            return false;
189
        }
190
191 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
192 12
        $iterator->rewind();
193
194 12
        return $iterator->valid();
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 42
    public function listChildren($path)
201
    {
202 42
        $filesystemPath = $this->getFilesystemPath($path);
203
204 26
        if (!is_dir($filesystemPath)) {
205 4
            return new FilesystemResourceCollection();
206
        }
207
208 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 359
    public function add($path, $resource)
215
    {
216 359
        $path = $this->sanitizePath($path);
217
218 347 View Code Duplication
        if ($resource instanceof ResourceCollection) {
219 4
            $this->ensureDirectoryExists($path);
220 4
            foreach ($resource as $child) {
221 4
                $this->addResource($path.'/'.$child->getName(), $child);
222
            }
223
224 4
            return;
225
        }
226
227 343
        if ($resource instanceof PuliResource) {
228 339
            $this->ensureDirectoryExists(Path::getDirectory($path));
229 339
            $this->addResource($path, $resource);
230
231 334
            return;
232
        }
233
234 4
        throw new UnsupportedResourceException(sprintf(
235 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
236 4
            is_object($resource) ? get_class($resource) : gettype($resource)
237
        ));
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 61
    public function remove($query, $language = 'glob')
244
    {
245 61
        $iterator = $this->getGlobIterator($query, $language);
246 48
        $removed = 0;
247
248 48
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
249
250
        // There's some problem with concurrent deletions at the moment
251 40
        foreach (iterator_to_array($iterator) as $filesystemPath) {
252 40
            $this->removeResource($filesystemPath, $removed);
253
        }
254
255 40
        return $removed;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 14
    public function clear()
262
    {
263 14
        $iterator = $this->getDirectoryIterator($this->baseDir);
264 14
        $removed = 0;
265
266
        // Batch-delete all versions
267 14
        $this->clearVersions();
268
269 14
        foreach ($iterator as $filesystemPath) {
270 10
            $this->removeResource($filesystemPath, $removed);
271
        }
272
273 14
        $this->storeVersion($this->get('/'));
274
275 14
        return $removed;
276
    }
277
278 343
    private function ensureDirectoryExists($path)
279
    {
280 343
        $filesystemPath = $this->baseDir.$path;
281
282 343
        if (is_file($filesystemPath)) {
283 1
            throw new UnsupportedOperationException(sprintf(
284
                'Instances of BodyResource do not support child resources in '.
285 1
                'FilesystemRepository. Tried to add a child to %s.',
286
                $filesystemPath
287
            ));
288
        }
289
290 343
        if (!is_dir($filesystemPath)) {
291 112
            mkdir($filesystemPath, 0777, true);
292
        }
293 343
    }
294
295 343
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
296
    {
297 343
        $pathInBaseDir = $this->baseDir.$path;
298 343
        $hasChildren = $resource->hasChildren();
299 343
        $hasBody = $resource instanceof BodyResource;
300
301 343
        if ($hasChildren && $hasBody) {
302 1
            throw new UnsupportedResourceException(sprintf(
303
                'Instances of BodyResource do not support child resources in '.
304
                'FilesystemRepository. Tried to add a BodyResource with '.
305 1
                'children at %s.',
306
                $path
307
            ));
308
        }
309
310 342
        $resource = clone $resource;
311 342
        $resource->attachTo($this, $path);
312
313 342
        if ($this->symlink && $checkParentsForSymlinks) {
314 184
            $this->replaceParentSymlinksByCopies($path);
315
        }
316
317 342
        if ($resource instanceof FilesystemResource) {
318 35
            if ($this->symlink) {
319 32
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
320 3
            } elseif ($hasBody) {
321 2
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
322
            } else {
323 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
324
            }
325
326 35
            $this->storeVersion($resource);
327
328 35
            return;
329
        }
330
331 315
        if ($resource instanceof LinkResource) {
332 8
            if (!$this->symlink) {
333 4
                throw new UnsupportedResourceException(sprintf(
334
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
335 4
                    'Tried to add a LinkResource at %s.',
336
                    $path
337
                ));
338
            }
339
340 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
341
342 4
            $this->storeVersion($resource);
343
344 4
            return;
345
        }
346
347 307
        if ($hasBody) {
348 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...
349
350 175
            $this->storeVersion($resource);
351
352 175
            return;
353
        }
354
355 239
        if (is_file($pathInBaseDir)) {
356
            $this->filesystem->remove($pathInBaseDir);
357
        }
358
359 239
        if (!file_exists($pathInBaseDir)) {
360 147
            mkdir($pathInBaseDir, 0777, true);
361
        }
362
363 239
        foreach ($resource->listChildren() as $child) {
364 148
            $this->addResource($path.'/'.$child->getName(), $child, false);
365
        }
366
367 239
        $this->storeVersion($resource);
368 239
    }
369
370 50
    private function removeResource($filesystemPath, &$removed)
371
    {
372
        // Skip paths that have already been removed
373 50
        if (!file_exists($filesystemPath)) {
374
            return;
375
        }
376
377 50
        $this->removeVersions($this->getPath($filesystemPath));
378
379 50
        ++$removed;
380
381 50
        if (is_dir($filesystemPath) && !is_link($filesystemPath)) {
382 32
            $iterator = $this->getDirectoryIterator($filesystemPath);
383
384 32
            foreach ($iterator as $childFilesystemPath) {
385
                // Remove children and child versions
386 32
                $this->removeResource($childFilesystemPath, $removed);
387
            }
388
        }
389
390 50
        $this->filesystem->remove($filesystemPath);
391 50
    }
392
393 149
    private function createResource($filesystemPath, $path)
394
    {
395 149
        $resource = null;
396
397 149
        if (is_link($filesystemPath)) {
398 6
            $baseDir = rtrim($this->baseDir, '/');
399 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
400
401 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
402 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
403 6
                $resource = new LinkResource($targetPath);
404
            }
405
        }
406
407 149
        if (!$resource && is_dir($filesystemPath)) {
408 100
            $resource = new DirectoryResource($filesystemPath);
409
        }
410
411 149
        if (!$resource) {
412 79
            $resource = new FileResource($filesystemPath);
413
        }
414
415 149
        $resource->attachTo($this, $path);
416
417 149
        return $resource;
418
    }
419
420 50
    private function iteratorToCollection(Iterator $iterator)
421
    {
422 50
        $filesystemPaths = iterator_to_array($iterator);
423 50
        $resources = array();
424
425
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
426 50
        sort($filesystemPaths);
427
428 50
        foreach ($filesystemPaths as $filesystemPath) {
429 42
            $resource = is_dir($filesystemPath)
430 24
                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))
431 42
                : new FileResource($filesystemPath, $this->getPath($filesystemPath));
432
433 42
            $resource->attachTo($this);
434
435 42
            $resources[] = $resource;
436
        }
437
438 50
        return new FilesystemResourceCollection($resources);
439
    }
440
441 66 View Code Duplication
    private function getFilesystemPath($path)
442
    {
443 66
        $path = $this->sanitizePath($path);
444 42
        $filesystemPath = $this->baseDir.$path;
445
446 42
        if (!file_exists($filesystemPath)) {
447 8
            throw ResourceNotFoundException::forPath($path);
448
        }
449
450 34
        return $filesystemPath;
451
    }
452
453 137
    private function getGlobIterator($query, $language)
454
    {
455 137
        $this->failUnlessGlob($language);
456
457 134
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
458 110
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
459
460 98
        $query = Path::canonicalize($query);
461
462 98
        return new GlobIterator($this->baseDir.$query);
463
    }
464
465 84
    private function getDirectoryIterator($filesystemPath)
466
    {
467 84
        return new RecursiveDirectoryIterator(
468
            $filesystemPath,
469 84
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
470
        );
471
    }
472
473 32
    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
474
    {
475 32
        $targetIsDir = is_dir($target);
476 32
        $forceDir = in_array($target, $dirsToKeep, true);
477
478
        // Merge directories
479 32
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
480 16
            if (is_link($target)) {
481 4
                $this->replaceLinkByCopy($target, $dirsToKeep);
482
            }
483
484 16
            $iterator = $this->getDirectoryIterator($origin);
485
486 16
            foreach ($iterator as $path) {
487 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
488
            }
489
490 16
            return;
491
        }
492
493
        // Replace target
494 32
        if (file_exists($target)) {
495 10
            $this->filesystem->remove($target);
496
        }
497
498
        // Try creating a relative link
499 32
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
500 16
            return;
501
        }
502
503
        // Try creating a absolute link
504 16
        if ($this->trySymlink($origin, $target)) {
505 16
            return;
506
        }
507
508
        // Fall back to copy
509
        if (is_dir($origin)) {
510
            $this->filesystem->mirror($origin, $target);
511
512
            return;
513
        }
514
515
        $this->filesystem->copy($origin, $target);
516
    }
517
518 184
    private function replaceParentSymlinksByCopies($path)
519
    {
520 184
        $previousPath = null;
521
522
        // Collect all paths that MUST NOT be symlinks after doing the
523
        // replace operation.
524
        //
525
        // Example:
526
        //
527
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
528
        //
529
        // Before:
530
        //   /webmozart -> target
531
        //
532
        // After:
533
        //   /webmozart
534
        //     /config -> target/config
535
        //     /views
536
        //       /index.html.twig -> target/views/index.html.twig
537
538 184
        $dirsToKeep = array();
539
540 184
        while ($previousPath !== ($path = Path::getDirectory($path))) {
541 184
            $filesystemPath = $this->baseDir.$path;
542 184
            $dirsToKeep[] = $filesystemPath;
543
544 184
            if (is_link($filesystemPath)) {
545 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
546
547 12
                return;
548
            }
549
550 184
            $previousPath = $path;
551
        }
552 184
    }
553
554 16
    private function replaceLinkByCopy($path, array $dirsToKeep = array())
555
    {
556 16
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
557 16
        $this->filesystem->remove($path);
558 16
        $this->filesystem->mkdir($path);
559 16
        $this->symlinkMirror($target, $path, $dirsToKeep);
560 16
    }
561
562 32
    private function trySymlink($origin, $target)
563
    {
564
        try {
565 32
            $this->filesystem->symlink($origin, $target);
566
567 32
            if (file_exists($target)) {
568 32
                return true;
569
            }
570
        } catch (IOException $e) {
571
        }
572
573
        return false;
574
    }
575
576 22
    private function readLink($filesystemPath)
577
    {
578
        // On Windows, transitive links are resolved to the final target by
579
        // readlink(). realpath(), however, returns the target link on Windows,
580
        // but not on Unix.
581
582
        // /link1 -> /link2 -> /file
583
584
        // Windows: readlink(/link1) => /file
585
        //          realpath(/link1) => /link2
586
587
        // Unix:    readlink(/link1) => /link2
588
        //          realpath(/link1) => /file
589
590
        // Consistency FTW!
591
592 22
        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
593
    }
594
595 92
    private function getPath($filesystemPath)
596
    {
597 92
        return substr($filesystemPath, $this->baseDirLength);
598
    }
599
}
600