Failed Conditions
Pull Request — 1.0 (#79)
by Bernhard
12:18
created

FilesystemRepository   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 541
Duplicated Lines 5.55 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 95.12%

Importance

Changes 12
Bugs 3 Features 3
Metric Value
wmc 79
c 12
b 3
f 3
lcom 2
cbo 15
dl 30
loc 541
ccs 234
cts 246
cp 0.9512
rs 2.0548

24 Methods

Rating   Name   Duplication   Size   Complexity  
A isSymlinkSupported() 0 15 3
A __construct() 0 13 3
A get() 11 11 2
A find() 0 4 1
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 78 15
B removeResource() 0 22 4
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 RecursiveIteratorIterator;
28
use Symfony\Component\Filesystem\Exception\IOException;
29
use Symfony\Component\Filesystem\Filesystem;
30
use Webmozart\Assert\Assert;
31
use Webmozart\Glob\Iterator\GlobIterator;
32
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
33
use Webmozart\PathUtil\Path;
34
35
/**
36
 * A repository reading from the file system.
37
 *
38
 * Resources can be read using their absolute file system paths:
39
 *
40
 * ```php
41
 * use Puli\Repository\FilesystemRepository;
42
 *
43
 * $repo = new FilesystemRepository();
44
 * $resource = $repo->get('/home/puli/.gitconfig');
45
 * ```
46
 *
47
 * The returned resources implement {@link FilesystemResource}.
48
 *
49
 * Optionally, a root directory can be passed to the constructor. Then all paths
50
 * will be read relative to that directory:
51
 *
52
 * ```php
53
 * $repo = new FilesystemRepository('/home/puli');
54
 * $resource = $repo->get('/.gitconfig');
55
 * ```
56
 *
57
 * While "." and ".." segments are supported, files outside the root directory
58
 * cannot be read. Any leading ".." segments will simply be stripped off.
59
 *
60
 * @since  1.0
61
 *
62
 * @author Bernhard Schussek <[email protected]>
63
 */
64
class FilesystemRepository extends AbstractEditableRepository
65
{
66
    /**
67
     * @var bool|null
68
     */
69
    private static $symlinkSupported;
70
71
    /**
72
     * @var string
73
     */
74
    private $baseDir;
75
76
    /**
77
     * @var int
78
     */
79
    private $baseDirLength;
80
81
    /**
82
     * @var bool
83
     */
84
    private $symlink;
85
86
    /**
87
     * @var bool
88
     */
89
    private $relative;
90
91
    /**
92
     * @var Filesystem
93
     */
94
    private $filesystem;
95
96
    /**
97
     * Returns whether symlinks are supported in the local environment.
98
     *
99
     * @return bool Returns `true` if symlinks are supported.
100
     */
101 303
    public static function isSymlinkSupported()
102
    {
103 303
        if (null === self::$symlinkSupported) {
104
            // http://php.net/manual/en/function.symlink.php
105
            // Symlinks are only supported on Windows Vista, Server 2008 or
106
            // greater on PHP 5.3+
107 1
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
108
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
109
            } else {
110 1
                self::$symlinkSupported = true;
111
            }
112 1
        }
113
114 303
        return self::$symlinkSupported;
115
    }
116
117
    /**
118
     * Creates a new repository.
119
     *
120
     * @param string            $baseDir      The base directory of the repository on the file
121
     *                                        system.
122
     * @param bool              $symlink      Whether to use symbolic links for added files. If
123
     *                                        symbolic links are not supported on the current
124
     *                                        system, the repository will create hard copies
125
     *                                        instead.
126
     * @param bool              $relative     Whether to create relative symbolic links. If
127
     *                                        relative links are not supported on the current
128
     *                                        system, the repository will create absolute links
129
     *                                        instead.
130
     * @param ChangeStream|null $changeStream If provided, the repository will log
131
     *                                        resources changes in this change stream.
132
     */
133 392
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
134
    {
135 392
        parent::__construct($changeStream);
136
137 392
        Assert::directory($baseDir);
138 392
        Assert::boolean($symlink);
139
140 392
        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
141 392
        $this->baseDirLength = strlen($baseDir);
142 392
        $this->symlink = $symlink && self::isSymlinkSupported();
143 392
        $this->relative = $this->symlink && $relative;
144 392
        $this->filesystem = new Filesystem();
145 392
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 159 View Code Duplication
    public function get($path)
151
    {
152 159
        $path = $this->sanitizePath($path);
153 147
        $filesystemPath = $this->baseDir.$path;
154
155 147
        if (!file_exists($filesystemPath)) {
156 8
            throw ResourceNotFoundException::forPath($path);
157
        }
158
159 139
        return $this->createResource($filesystemPath, $path);
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 41
    public function find($query, $language = 'glob')
166
    {
167 41
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 61
    public function contains($query, $language = 'glob')
174
    {
175 61
        $iterator = $this->getGlobIterator($query, $language);
176 48
        $iterator->rewind();
177
178 48
        return $iterator->valid();
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 28
    public function hasChildren($path)
185
    {
186 28
        $filesystemPath = $this->getFilesystemPath($path);
187
188 12
        if (!is_dir($filesystemPath)) {
189 4
            return false;
190
        }
191
192 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
193 12
        $iterator->rewind();
194
195 12
        return $iterator->valid();
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 42
    public function listChildren($path)
202
    {
203 42
        $filesystemPath = $this->getFilesystemPath($path);
204
205 26
        if (!is_dir($filesystemPath)) {
206 4
            return new FilesystemResourceCollection();
207
        }
208
209 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215 357
    public function add($path, $resource)
216
    {
217 357
        $path = $this->sanitizePath($path);
218
219 345 View Code Duplication
        if ($resource instanceof ResourceCollection) {
220 4
            $this->ensureDirectoryExists($path);
221 4
            foreach ($resource as $child) {
222 4
                $this->addResource($path.'/'.$child->getName(), $child);
223 4
            }
224
225 4
            return;
226
        }
227
228 341
        if ($resource instanceof PuliResource) {
229 337
            $this->ensureDirectoryExists(Path::getDirectory($path));
230 337
            $this->addResource($path, $resource);
231
232 332
            return;
233
        }
234
235 4
        throw new UnsupportedResourceException(sprintf(
236 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
237 4
            is_object($resource) ? get_class($resource) : gettype($resource)
238 4
        ));
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244 61
    public function remove($query, $language = 'glob')
245
    {
246 61
        $iterator = $this->getGlobIterator($query, $language);
247 48
        $removed = 0;
248
249 48
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
250
251
        // There's some problem with concurrent deletions at the moment
252 40
        foreach (iterator_to_array($iterator) as $filesystemPath) {
253 40
            $this->removeResource($filesystemPath, $removed);
254 40
        }
255
256 40
        return $removed;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262 12
    public function clear()
263
    {
264 12
        $iterator = $this->getDirectoryIterator($this->baseDir);
265 12
        $removed = 0;
266
267
        // Batch-delete all versions
268 12
        $this->clearVersions();
269
270 12
        foreach ($iterator as $filesystemPath) {
271 8
            $this->removeResource($filesystemPath, $removed);
272 12
        }
273
274 12
        $this->storeVersion($this->get('/'));
275
276 12
        return $removed;
277
    }
278
279 341
    private function ensureDirectoryExists($path)
280
    {
281 341
        $filesystemPath = $this->baseDir.$path;
282
283 341
        if (is_file($filesystemPath)) {
284 1
            throw new UnsupportedOperationException(sprintf(
285
                'Instances of BodyResource do not support child resources in '.
286 1
                'FilesystemRepository. Tried to add a child to %s.',
287
                $filesystemPath
288 1
            ));
289
        }
290
291 341
        if (!is_dir($filesystemPath)) {
292 120
            mkdir($filesystemPath, 0777, true);
293 120
        }
294 341
    }
295
296 341
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
297
    {
298 341
        $pathInBaseDir = $this->baseDir.$path;
299 341
        $hasChildren = $resource->hasChildren();
300 341
        $hasBody = $resource instanceof BodyResource;
301
302 341
        if ($hasChildren && $hasBody) {
303 1
            throw new UnsupportedResourceException(sprintf(
304
                'Instances of BodyResource do not support child resources in '.
305 1
                'FilesystemRepository. Tried to add a BodyResource with '.
306 1
                'children at %s.',
307
                $path
308 1
            ));
309
        }
310
311
        // Don't modify resources attached to other repositories
312 340
        if ($resource->isAttached()) {
313 4
            $resource = clone $resource;
314 4
        }
315
316 340
        $resource->attachTo($this, $path);
317
318 340
        if ($this->symlink && $checkParentsForSymlinks) {
319 182
            $this->replaceParentSymlinksByCopies($path);
320 182
        }
321
322 340
        if ($resource instanceof FilesystemResource) {
323 33
            if ($this->symlink) {
324 30
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
325 33
            } elseif ($hasBody) {
326 2
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
327 2
            } else {
328 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
329
            }
330
331 33
            $this->storeVersion($resource);
332
333 33
            return;
334
        }
335
336 315
        if ($resource instanceof LinkResource) {
337 8
            if (!$this->symlink) {
338 4
                throw new UnsupportedResourceException(sprintf(
339
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
340 4
                    'Tried to add a LinkResource at %s.',
341
                    $path
342 4
                ));
343
            }
344
345 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
346
347 4
            $this->storeVersion($resource);
348
349 4
            return;
350 1
        }
351
352 307
        if ($hasBody) {
353 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...
354
355 175
            $this->storeVersion($resource);
356
357 175
            return;
358
        }
359
360 231
        if (is_file($pathInBaseDir)) {
361
            $this->filesystem->remove($pathInBaseDir);
362
        }
363
364 231
        if (!file_exists($pathInBaseDir)) {
365 139
            mkdir($pathInBaseDir, 0777, true);
366 139
        }
367
368 231
        foreach ($resource->listChildren() as $child) {
369 140
            $this->addResource($path.'/'.$child->getName(), $child, false);
370 231
        }
371
372 231
        $this->storeVersion($resource);
373 231
    }
374
375 48
    private function removeResource($filesystemPath, &$removed)
376
    {
377
        // Skip paths that have already been removed
378 48
        if (!file_exists($filesystemPath)) {
379
            return;
380
        }
381
382 48
        $this->removeVersions($this->getPath($filesystemPath));
383
384 48
        ++$removed;
385
386 48
        if (is_dir($filesystemPath)) {
387 32
            $iterator = $this->getDirectoryIterator($filesystemPath);
388
389 32
            foreach ($iterator as $childFilesystemPath) {
390
                // Remove children and child versions
391 32
                $this->removeResource($childFilesystemPath, $removed);
392 32
            }
393 32
        }
394
395 48
        $this->filesystem->remove($filesystemPath);
396 48
    }
397
398 139
    private function createResource($filesystemPath, $path)
399
    {
400 139
        $resource = null;
401
402 139
        if (is_link($filesystemPath)) {
403 6
            $baseDir = rtrim($this->baseDir, '/');
404 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
405
406 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
407 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
408 6
                $resource = new LinkResource($targetPath);
409 6
            }
410 6
        }
411
412 139
        if (!$resource && is_dir($filesystemPath)) {
413 90
            $resource = new DirectoryResource($filesystemPath);
414 90
        }
415
416 139
        if (!$resource) {
417 71
            $resource = new FileResource($filesystemPath);
418 71
        }
419
420 139
        $resource->attachTo($this, $path);
421
422 139
        return $resource;
423
    }
424
425 50
    private function iteratorToCollection(Iterator $iterator)
426
    {
427 50
        $filesystemPaths = iterator_to_array($iterator);
428 50
        $resources = array();
429
430
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
431 50
        sort($filesystemPaths);
432
433 50
        foreach ($filesystemPaths as $filesystemPath) {
434 42
            $resource = is_dir($filesystemPath)
435 42
                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))
436 42
                : new FileResource($filesystemPath, $this->getPath($filesystemPath));
437
438 42
            $resource->attachTo($this);
439
440 42
            $resources[] = $resource;
441 50
        }
442
443 50
        return new FilesystemResourceCollection($resources);
444
    }
445
446 66 View Code Duplication
    private function getFilesystemPath($path)
447
    {
448 66
        $path = $this->sanitizePath($path);
449 42
        $filesystemPath = $this->baseDir.$path;
450
451 42
        if (!file_exists($filesystemPath)) {
452 8
            throw ResourceNotFoundException::forPath($path);
453
        }
454
455 34
        return $filesystemPath;
456
    }
457
458 135
    private function getGlobIterator($query, $language)
459
    {
460 135
        $this->failUnlessGlob($language);
461
462 132
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
463 108
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
464
465 96
        $query = Path::canonicalize($query);
466
467 96
        return new GlobIterator($this->baseDir.$query);
468
    }
469
470 82
    private function getDirectoryIterator($filesystemPath)
471
    {
472 82
        return new RecursiveDirectoryIterator(
473 82
            $filesystemPath,
474 82
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
475 82
        );
476
    }
477
478 30
    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
479
    {
480 30
        $targetIsDir = is_dir($target);
481 30
        $forceDir = in_array($target, $dirsToKeep, true);
482
483
        // Merge directories
484 30
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
485 16
            if (is_link($target)) {
486 4
                $this->replaceLinkByCopy($target, $dirsToKeep);
487 4
            }
488
489 16
            $iterator = $this->getDirectoryIterator($origin);
490
491 16
            foreach ($iterator as $path) {
492 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
493 16
            }
494
495 16
            return;
496
        }
497
498
        // Replace target
499 30
        if (file_exists($target)) {
500 10
            $this->filesystem->remove($target);
501 10
        }
502
503
        // Try creating a relative link
504 30
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
505 15
            return;
506
        }
507
508
        // Try creating a absolute link
509 15
        if ($this->trySymlink($origin, $target)) {
510 15
            return;
511
        }
512
513
        // Fall back to copy
514
        if (is_dir($origin)) {
515
            $this->filesystem->mirror($origin, $target);
516
517
            return;
518
        }
519
520
        $this->filesystem->copy($origin, $target);
521
    }
522
523 182
    private function replaceParentSymlinksByCopies($path)
524
    {
525 182
        $previousPath = null;
526
527
        // Collect all paths that MUST NOT be symlinks after doing the
528
        // replace operation.
529
        //
530
        // Example:
531
        //
532
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
533
        //
534
        // Before:
535
        //   /webmozart -> target
536
        //
537
        // After:
538
        //   /webmozart
539
        //     /config -> target/config
540
        //     /views
541
        //       /index.html.twig -> target/views/index.html.twig
542
543 182
        $dirsToKeep = array();
544
545 182
        while ($previousPath !== ($path = Path::getDirectory($path))) {
546 182
            $filesystemPath = $this->baseDir.$path;
547 182
            $dirsToKeep[] = $filesystemPath;
548
549 182
            if (is_link($filesystemPath)) {
550 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
551
552 12
                return;
553
            }
554
555 182
            $previousPath = $path;
556 182
        }
557 182
    }
558
559 16
    private function replaceLinkByCopy($path, array $dirsToKeep = array())
560
    {
561 16
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
562 16
        $this->filesystem->remove($path);
563 16
        $this->filesystem->mkdir($path);
564 16
        $this->symlinkMirror($target, $path, $dirsToKeep);
565 16
    }
566
567 30
    private function trySymlink($origin, $target)
568
    {
569
        try {
570 30
            $this->filesystem->symlink($origin, $target);
571
572 30
            if (file_exists($target)) {
573 30
                return true;
574
            }
575
        } catch (IOException $e) {
576
        }
577
578
        return false;
579
    }
580
581 22
    private function readLink($filesystemPath)
582
    {
583
        // On Windows, transitive links are resolved to the final target by
584
        // readlink(). realpath(), however, returns the target link on Windows,
585
        // but not on Unix.
586
587
        // /link1 -> /link2 -> /file
588
589
        // Windows: readlink(/link1) => /file
590
        //          realpath(/link1) => /link2
591
592
        // Unix:    readlink(/link1) => /link2
593
        //          realpath(/link1) => /file
594
595
        // Consistency FTW!
596
597 22
        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
598
    }
599
600 90
    private function getPath($filesystemPath)
601
    {
602 90
        return substr($filesystemPath, $this->baseDirLength);
603
    }
604
}
605