Completed
Push — 1.0 ( 0e2cc5...14cdbe )
by Bernhard
02:42
created

FilesystemRepository::addResource()   C

Complexity

Conditions 15
Paths 57

Size

Total Lines 78
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 15.0163

Importance

Changes 5
Bugs 1 Features 3
Metric Value
c 5
b 1
f 3
dl 0
loc 78
ccs 46
cts 48
cp 0.9583
rs 5.15
cc 15
eloc 44
nc 57
nop 3
crap 15.0163

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 bool
78
     */
79
    private $symlink;
80
81
    /**
82
     * @var bool
83
     */
84
    private $relative;
85
86
    /**
87
     * @var Filesystem
88
     */
89
    private $filesystem;
90
91
    /**
92
     * Returns whether symlinks are supported in the local environment.
93
     *
94
     * @return bool Returns `true` if symlinks are supported.
95
     */
96 267
    public static function isSymlinkSupported()
97
    {
98 267
        if (null === self::$symlinkSupported) {
99
            // http://php.net/manual/en/function.symlink.php
100
            // Symlinks are only supported on Windows Vista, Server 2008 or
101
            // greater on PHP 5.3+
102 1
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
103
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
104
            } else {
105 1
                self::$symlinkSupported = true;
106
            }
107 1
        }
108
109 267
        return self::$symlinkSupported;
110
    }
111
112
    /**
113
     * Creates a new repository.
114
     *
115
     * @param string            $baseDir      The base directory of the repository on the file
116
     *                                        system.
117
     * @param bool              $symlink      Whether to use symbolic links for added files. If
118
     *                                        symbolic links are not supported on the current
119
     *                                        system, the repository will create hard copies
120
     *                                        instead.
121
     * @param bool              $relative     Whether to create relative symbolic links. If
122
     *                                        relative links are not supported on the current
123
     *                                        system, the repository will create absolute links
124
     *                                        instead.
125
     * @param ChangeStream|null $changeStream If provided, the repository will log
126
     *                                        resources changes in this change stream.
127
     */
128 344
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
129
    {
130 344
        parent::__construct($changeStream);
131
132 344
        Assert::directory($baseDir);
133 344
        Assert::boolean($symlink);
134
135 344
        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
136 344
        $this->symlink = $symlink && self::isSymlinkSupported();
137 344
        $this->relative = $this->symlink && $relative;
138 344
        $this->filesystem = new Filesystem();
139 344
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144 135 View Code Duplication
    public function get($path)
145
    {
146 135
        $path = $this->sanitizePath($path);
147 123
        $filesystemPath = $this->baseDir.$path;
148
149 123
        if (!file_exists($filesystemPath)) {
150 4
            throw ResourceNotFoundException::forPath($path);
151
        }
152
153 119
        return $this->createResource($filesystemPath, $path);
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 41
    public function find($query, $language = 'glob')
160
    {
161 41
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 61
    public function contains($query, $language = 'glob')
168
    {
169 61
        $iterator = $this->getGlobIterator($query, $language);
170 48
        $iterator->rewind();
171
172 48
        return $iterator->valid();
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 28
    public function hasChildren($path)
179
    {
180 28
        $filesystemPath = $this->getFilesystemPath($path);
181
182 12
        if (!is_dir($filesystemPath)) {
183 4
            return false;
184
        }
185
186 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
187 12
        $iterator->rewind();
188
189 12
        return $iterator->valid();
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 42
    public function listChildren($path)
196
    {
197 42
        $filesystemPath = $this->getFilesystemPath($path);
198
199 26
        if (!is_dir($filesystemPath)) {
200 4
            return new FilesystemResourceCollection();
201
        }
202
203 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 305
    public function add($path, $resource)
210
    {
211 305
        $path = $this->sanitizePath($path);
212
213 293 View Code Duplication
        if ($resource instanceof ResourceCollection) {
214 4
            $this->ensureDirectoryExists($path);
215 4
            foreach ($resource as $child) {
216 4
                $this->addResource($path.'/'.$child->getName(), $child);
217 4
            }
218
219 4
            return;
220
        }
221
222 289
        if ($resource instanceof PuliResource) {
223 285
            $this->ensureDirectoryExists(Path::getDirectory($path));
224 285
            $this->addResource($path, $resource);
225
226 280
            return;
227
        }
228
229 4
        throw new UnsupportedResourceException(sprintf(
230 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
231 4
            is_object($resource) ? get_class($resource) : gettype($resource)
232 4
        ));
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 49
    public function remove($query, $language = 'glob')
239
    {
240 49
        $iterator = $this->getGlobIterator($query, $language);
241 36
        $removed = 0;
242
243 36
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
244
245
        // There's some problem with concurrent deletions at the moment
246 28
        foreach (iterator_to_array($iterator) as $filesystemPath) {
247 28
            $this->removeResource($filesystemPath, $removed);
248 28
        }
249
250 28
        return $removed;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 4
    public function clear()
257
    {
258 4
        $iterator = $this->getDirectoryIterator($this->baseDir);
259 4
        $removed = 0;
260
261 4
        foreach ($iterator as $filesystemPath) {
262 4
            $this->removeResource($filesystemPath, $removed);
263 4
        }
264
265 4
        return $removed;
266
    }
267
268 289
    private function ensureDirectoryExists($path)
269
    {
270 289
        $filesystemPath = $this->baseDir.$path;
271
272 289
        if (is_file($filesystemPath)) {
273 1
            throw new UnsupportedOperationException(sprintf(
274
                'Instances of BodyResource do not support child resources in '.
275 1
                'FilesystemRepository. Tried to add a child to %s.',
276
                $filesystemPath
277 1
            ));
278
        }
279
280 289
        if (!is_dir($filesystemPath)) {
281 88
            mkdir($filesystemPath, 0777, true);
282 88
        }
283 289
    }
284
285 289
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
286
    {
287 289
        $pathInBaseDir = $this->baseDir.$path;
288 289
        $hasChildren = $resource->hasChildren();
289 289
        $hasBody = $resource instanceof BodyResource;
290
291 289
        if ($hasChildren && $hasBody) {
292 1
            throw new UnsupportedResourceException(sprintf(
293
                'Instances of BodyResource do not support child resources in '.
294 1
                'FilesystemRepository. Tried to add a BodyResource with '.
295 1
                'children at %s.',
296
                $path
297 1
            ));
298
        }
299
300
        // Don't modify resources attached to other repositories
301 288
        if ($resource->isAttached()) {
302 4
            $resource = clone $resource;
303 4
        }
304
305 288
        $resource->attachTo($this, $path);
306
307 288
        if ($this->symlink && $checkParentsForSymlinks) {
308 156
            $this->replaceParentSymlinksByCopies($path);
309 156
        }
310
311 288
        if ($resource instanceof FilesystemResource) {
312 37
            if ($this->symlink) {
313 32
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
314 37
            } elseif ($hasBody) {
315 4
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
316 4
            } else {
317 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
318
            }
319
320 37
            $this->appendToChangeStream($resource);
321
322 37
            return;
323
        }
324
325 259
        if ($resource instanceof LinkResource) {
326 8
            if (!$this->symlink) {
327 4
                throw new UnsupportedResourceException(sprintf(
328
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
329 4
                    'Tried to add a LinkResource at %s.',
330
                    $path
331 4
                ));
332
            }
333
334 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
335
336 4
            $this->appendToChangeStream($resource);
337
338 4
            return;
339
        }
340
341 251
        if ($hasBody) {
342 127
            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...
343
344 127
            $this->appendToChangeStream($resource);
345
346 127
            return;
347
        }
348
349 191
        if (is_file($pathInBaseDir)) {
350
            $this->filesystem->remove($pathInBaseDir);
351
        }
352
353 191
        if (!file_exists($pathInBaseDir)) {
354 115
            mkdir($pathInBaseDir, 0777, true);
355 115
        }
356
357 191
        foreach ($resource->listChildren() as $child) {
358 112
            $this->addResource($path.'/'.$child->getName(), $child, false);
359 191
        }
360
361 191
        $this->appendToChangeStream($resource);
362 191
    }
363
364 32
    private function removeResource($filesystemPath, &$removed)
365
    {
366
        // Skip paths that have already been removed
367 32
        if (!file_exists($filesystemPath)) {
368
            return;
369
        }
370
371 32
        ++$removed;
372
373 32
        if (is_dir($filesystemPath)) {
374 24
            $removed += $this->countChildren($filesystemPath);
375 24
        }
376
377 32
        $this->filesystem->remove($filesystemPath);
378 32
    }
379
380 119
    private function createResource($filesystemPath, $path)
381
    {
382 119
        $resource = null;
383
384 119
        if (is_link($filesystemPath)) {
385 6
            $baseDir = rtrim($this->baseDir, '/');
386 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
387
388 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
389 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
390 6
                $resource = new LinkResource($targetPath);
391 6
            }
392 6
        }
393
394 119
        if (!$resource && is_dir($filesystemPath)) {
395 74
            $resource = new DirectoryResource($filesystemPath);
396 74
        }
397
398 119
        if (!$resource) {
399 67
            $resource = new FileResource($filesystemPath);
400 67
        }
401
402 119
        $resource->attachTo($this, $path);
403
404 119
        return $resource;
405
    }
406
407 25
    private function countChildren($filesystemPath)
408
    {
409 24
        $iterator = new RecursiveIteratorIterator(
410 24
            $this->getDirectoryIterator($filesystemPath),
411
            RecursiveIteratorIterator::SELF_FIRST
412 25
        );
413
414 24
        $iterator->rewind();
415 24
        $count = 0;
416
417 24
        while ($iterator->valid()) {
418 24
            ++$count;
419 24
            $iterator->next();
420 24
        }
421
422 24
        return $count;
423
    }
424
425 50
    private function iteratorToCollection(Iterator $iterator)
426
    {
427 50
        $offset = strlen($this->baseDir);
428 50
        $filesystemPaths = iterator_to_array($iterator);
429 50
        $resources = array();
430
431
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
432 50
        sort($filesystemPaths);
433
434 50
        foreach ($filesystemPaths as $filesystemPath) {
435 42
            $path = substr($filesystemPath, $offset);
436
437 42
            $resource = is_dir($filesystemPath)
438 42
                ? new DirectoryResource($filesystemPath, $path)
439 42
                : new FileResource($filesystemPath, $path);
440
441 42
            $resource->attachTo($this);
442
443 42
            $resources[] = $resource;
444 50
        }
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 123
    private function getGlobIterator($query, $language)
462
    {
463 123
        $this->validateSearchLanguage($language);
464
465 120
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
466 96
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
467
468 84
        $query = Path::canonicalize($query);
469
470 84
        return new GlobIterator($this->baseDir.$query);
471
    }
472
473 70
    private function getDirectoryIterator($filesystemPath)
474
    {
475 70
        return new RecursiveDirectoryIterator(
476 70
            $filesystemPath,
477 70
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
478 70
        );
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 4
            }
491
492 16
            $iterator = $this->getDirectoryIterator($origin);
493
494 16
            foreach ($iterator as $path) {
495 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
496 16
            }
497
498 16
            return;
499
        }
500
501
        // Replace target
502 32
        if (file_exists($target)) {
503 12
            $this->filesystem->remove($target);
504 12
        }
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 156
    private function replaceParentSymlinksByCopies($path)
527
    {
528 156
        $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 156
        $dirsToKeep = array();
547
548 156
        while ($previousPath !== ($path = Path::getDirectory($path))) {
549 156
            $filesystemPath = $this->baseDir.$path;
550 156
            $dirsToKeep[] = $filesystemPath;
551
552 156
            if (is_link($filesystemPath)) {
553 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
554
555 12
                return;
556
            }
557
558 156
            $previousPath = $path;
559 156
        }
560 156
    }
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