Failed Conditions
Pull Request — 1.0 (#79)
by Bernhard
06:38 queued 03:08
created

AbstractJsonRepository::addFilesystemResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4286
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
/*
4
 * This file is part of the puli/repository package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Repository;
13
14
use Puli\Repository\Api\ChangeStream\ChangeStream;
15
use Puli\Repository\Api\Resource\FilesystemResource;
16
use Puli\Repository\Api\Resource\PuliResource;
17
use Puli\Repository\Api\ResourceCollection;
18
use Puli\Repository\Api\ResourceNotFoundException;
19
use Puli\Repository\Api\UnsupportedResourceException;
20
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
21
use Puli\Repository\Resource\DirectoryResource;
22
use Puli\Repository\Resource\FileResource;
23
use Puli\Repository\Resource\GenericResource;
24
use Puli\Repository\Resource\LinkResource;
25
use RuntimeException;
26
use Webmozart\Assert\Assert;
27
use Webmozart\Json\JsonDecoder;
28
use Webmozart\Json\JsonEncoder;
29
use Webmozart\PathUtil\Path;
30
31
/**
32
 * Abstract base for Path mapping repositories.
33
 *
34
 * @since  1.0
35
 *
36
 * @author Bernhard Schussek <[email protected]>
37
 * @author Titouan Galopin <[email protected]>
38
 */
39
abstract class AbstractJsonRepository extends AbstractEditableRepository
40
{
41
    /**
42
     * @var array
43
     */
44
    protected $json;
45
46
    /**
47
     * @var string
48
     */
49
    protected $baseDirectory;
50
51
    /**
52
     * @var string
53
     */
54
    private $path;
55
56
    /**
57
     * @var string
58
     */
59
    private $schemaPath;
60
61
    /**
62
     * @var JsonEncoder
63
     */
64
    private $encoder;
65
66
    /**
67
     * Creates a new repository.
68
     *
69
     * @param string            $path          The path to the JSON file. If
70
     *                                         relative, it must be relative to
71
     *                                         the base directory.
72
     * @param string            $baseDirectory The base directory of the store.
73
     *                                         Paths inside that directory are
74
     *                                         stored as relative paths. Paths
75
     *                                         outside that directory are stored
76
     *                                         as absolute paths.
77
     * @param bool              $validateJson  Whether to validate the JSON file
78
     *                                         against the schema. Slow but
79
     *                                         spots problems.
80
     * @param ChangeStream|null $changeStream  If provided, the repository will
81
     *                                         append resource changes to this
82
     *                                         change stream.
83
     */
84 183
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
85
    {
86 183
        parent::__construct($changeStream);
87
88 183
        $this->baseDirectory = $baseDirectory;
89 183
        $this->path = Path::makeAbsolute($path, $baseDirectory);
90 183
        $this->encoder = new JsonEncoder();
91
92 183
        if ($validateJson) {
93 183
            $this->schemaPath = realpath(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
94 183
        }
95 183
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 158
    public function add($path, $resource)
101
    {
102 158
        if (null === $this->json) {
103 158
            $this->load();
104 158
        }
105
106 158
        $path = $this->sanitizePath($path);
107
108 152 View Code Duplication
        if ($resource instanceof ResourceCollection) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109 2
            $this->ensureDirectoryExists($path);
110
111 2
            foreach ($resource as $child) {
112 2
                $this->addResource($path.'/'.$child->getName(), $child);
113 2
            }
114
115 2
            $this->flush();
116
117 2
            return;
118
        }
119
120 150
        $this->ensureDirectoryExists(Path::getDirectory($path));
121 150
        $this->addResource($path, $resource);
0 ignored issues
show
Documentation introduced by
$resource is of type object<Puli\Repository\Api\Resource\PuliResource>, but the function expects a object<Puli\Repository\A...\Resource\LinkResource>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
122
123 148
        $this->flush();
124 148
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 75
    public function get($path)
130
    {
131 75
        if (null === $this->json) {
132 21
            $this->load();
133 21
        }
134
135 75
        $path = $this->sanitizePath($path);
136 69
        $references = $this->getReferencesForPath($path);
137
138
        // Might be null, don't use isset()
139 69
        if (array_key_exists($path, $references)) {
140 65
            return $this->createResource($path, $references[$path]);
141
        }
142
143 4
        throw ResourceNotFoundException::forPath($path);
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 24
    public function find($query, $language = 'glob')
150
    {
151 24
        if (null === $this->json) {
152 2
            $this->load();
153 2
        }
154
155 24
        $this->validateSearchLanguage($language);
156 22
        $query = $this->sanitizePath($query);
157 16
        $results = $this->createResources($this->getReferencesForGlob($query));
158
159 11
        ksort($results);
160
161 11
        return new ArrayResourceCollection(array_values($results));
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 40
    public function contains($query, $language = 'glob')
168
    {
169 40
        if (null === $this->json) {
170 21
            $this->load();
171 21
        }
172
173 40
        $this->validateSearchLanguage($language);
174 38
        $query = $this->sanitizePath($query);
175
176
        // Stop on the first result
177 32
        $results = $this->getReferencesForGlob($query, true);
178
179 32
        return !empty($results);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 91
    public function remove($query, $language = 'glob')
186
    {
187 91
        if (null === $this->json) {
188 11
            $this->load();
189 11
        }
190
191 25
        $this->validateSearchLanguage($language);
192 24
        $query = $this->sanitizePath($query);
193
194 18
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
195
196 14
        $removed = $this->removeReferences($query);
197
198 14
        $this->flush();
199
200 14
        return $removed;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206 2
    public function clear()
207
    {
208 2
        if (null === $this->json) {
209
            $this->load();
210
        }
211
212
        // Subtract root which is not deleted
213 2
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;
214
215 2
        $this->json = array();
216
217 2
        $this->flush();
218
219 2
        return $removed;
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 27
    public function listChildren($path)
226
    {
227 27
        if (null === $this->json) {
228 3
            $this->load();
229 3
        }
230
231 27
        $path = $this->sanitizePath($path);
232 21
        $results = $this->createResources($this->getReferencesInDirectory($path));
233
234 17
        if (empty($results)) {
235 6
            $pathResults = $this->getReferencesForPath($path);
236
237 6
            if (empty($pathResults)) {
238 2
                throw ResourceNotFoundException::forPath($path);
239
            }
240 4
        }
241
242 15
        ksort($results);
243
244 15
        return new ArrayResourceCollection(array_values($results));
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 14
    public function hasChildren($path)
251
    {
252 14
        if (null === $this->json) {
253
            $this->load();
254
        }
255
256 14
        $path = $this->sanitizePath($path);
257
258
        // Stop on the first result
259 8
        $results = $this->getReferencesInDirectory($path, true);
260
261 8
        if (empty($results)) {
262 4
            $pathResults = $this->getReferencesForPath($path);
263
264 4
            if (empty($pathResults)) {
265 2
                throw ResourceNotFoundException::forPath($path);
266
            }
267
268 2
            return false;
269
        }
270
271 6
        return true;
272
    }
273
274
    /**
275
     * Inserts a path reference into the JSON file.
276
     *
277
     * The path reference can be:
278
     *
279
     *  * a link starting with `@`
280
     *  * an absolute filesystem path
281
     *
282
     * @param string      $path      The Puli path.
283
     * @param string|null $reference The path reference.
284
     */
285
    abstract protected function insertReference($path, $reference);
1 ignored issue
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
286
287
    /**
288
     * Removes all path references matching the given glob from the JSON file.
289
     *
290
     * @param string $glob The glob for a list of Puli paths.
291
     */
292
    abstract protected function removeReferences($glob);
1 ignored issue
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
293
294
    /**
295
     * Returns the references for a given Puli path.
296
     *
297
     * Each reference returned by this method can be:
298
     *
299
     *  * `null`
300
     *  * a link starting with `@`
301
     *  * an absolute filesystem path
302
     *
303
     * The result has either one entry or none, if no path was found. The key
304
     * of the single entry is the path passed to this method.
305
     *
306
     * @param string $path The Puli path.
307
     *
308
     * @return string[]|null[] A one-level array of references with Puli paths
309
     *                         as keys. The array has at most one entry.
310
     */
311
    abstract protected function getReferencesForPath($path);
312
313
    /**
314
     * Returns the references matching a given Puli path glob.
315
     *
316
     * Each reference returned by this method can be:
317
     *
318
     *  * `null`
319
     *  * a link starting with `@`
320
     *  * an absolute filesystem path
321
     *
322
     * The keys of the returned array are Puli paths. Their order is undefined.
323
     *
324
     * @param string $glob        The glob.
325
     * @param bool   $stopOnFirst Whether to stop after finding a first result.
326
     *
327
     * @return string[]|null[] A one-level array of references with Puli paths
328
     *                         as keys.
329
     */
330
    abstract protected function getReferencesForGlob($glob, $stopOnFirst = false);
331
332
    /**
333
     * Returns the references matching a given Puli path regular expression.
334
     *
335
     * Each reference returned by this method can be:
336
     *
337
     *  * `null`
338
     *  * a link starting with `@`
339
     *  * an absolute filesystem path
340
     *
341
     * The keys of the returned array are Puli paths. Their order is undefined.
342
     *
343
     * @param string $staticPrefix The static prefix of all Puli paths matching
344
     *                             the regular expression.
345
     * @param string $regex        The regular expression.
346
     * @param bool   $stopOnFirst  Whether to stop after finding a first result.
347
     *
348
     * @return string[]|null[] A one-level array of references with Puli paths
349
     *                         as keys.
350
     */
351
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $stopOnFirst = false);
352
353
    /**
354
     * Returns the references in a given Puli path.
355
     *
356
     * Each reference returned by this method can be:
357
     *
358
     *  * `null`
359
     *  * a link starting with `@`
360
     *  * an absolute filesystem path
361
     *
362
     * The keys of the returned array are Puli paths. Their order is undefined.
363
     *
364
     * @param string $path        The Puli path.
365
     * @param bool   $stopOnFirst Whether to stop after finding a first result.
366
     *
367
     * @return string[]|null[] A one-level array of references with Puli paths
368
     *                         as keys.
369
     */
370
    abstract protected function getReferencesInDirectory($path, $stopOnFirst = false);
371
372
    /**
373
     * Adds a filesystem resource to the JSON file.
374
     *
375
     * @param string             $path     The Puli path.
376
     * @param FilesystemResource $resource The resource to add.
377
     */
378 150
    protected function addFilesystemResource($path, FilesystemResource $resource)
379
    {
380 150
        $resource->attachTo($this, $path);
381
382 150
        $this->insertReference($path, $resource->getFilesystemPath());
383
384 150
        $this->appendToChangeStream($resource);
385 150
    }
386
387
    /**
388
     * Loads the JSON file.
389
     */
390 181
    protected function load()
391
    {
392 181
        $decoder = new JsonDecoder();
393
394 181
        $this->json = file_exists($this->path)
395 181
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
396 181
            : array();
397
398 181
        if (isset($this->json['_order'])) {
399 2
            $this->json['_order'] = (array) $this->json['_order'];
400
401 2
            foreach ($this->json['_order'] as $path => $entries) {
402 2
                foreach ($entries as $key => $entry) {
403 2
                    $this->json['_order'][$path][$key] = (array) $entry;
404 2
                }
405 2
            }
406 2
        }
407
408
        // The root node always exists
409 181
        if (!isset($this->json['/'])) {
410 181
            $this->json['/'] = null;
411 181
        }
412
413
        // Make sure the JSON is sorted in reverse order
414 181
        krsort($this->json);
415 181
    }
416
417
    /**
418
     * Writes the JSON file.
419
     */
420 150
    protected function flush()
421
    {
422
        // The root node always exists
423 150
        if (!isset($this->json['/'])) {
424 54
            $this->json['/'] = null;
425 54
        }
426
427
        // Always save in reverse order
428 150
        krsort($this->json);
429
430
        // Comply to schema
431 150
        $json = (object) $this->json;
432
433 150
        if (isset($json->{'_order'})) {
434 3
            foreach ($json->{'_order'} as $path => $entries) {
435 3
                foreach ($entries as $key => $entry) {
436 3
                    $json->{'_order'}[$path][$key] = (object) $entry;
437 3
                }
438 3
            }
439
440 3
            $json->{'_order'} = (object) $json->{'_order'};
441 3
        }
442
443 150
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
444 150
    }
445
446
    /**
447
     * Returns whether a reference contains a link.
448
     *
449
     * @param string $reference The reference.
450
     *
451
     * @return bool Whether the reference contains a link.
452
     */
453 131
    protected function isLinkReference($reference)
454
    {
455 131
        return isset($reference{0}) && '@' === $reference{0};
456
    }
457
458
    /**
459
     * Returns whether a reference contains an absolute or relative filesystem
460
     * path.
461
     *
462
     * @param string $reference The reference.
463
     *
464
     * @return bool Whether the reference contains a filesystem path.
465
     */
466 132
    protected function isFilesystemReference($reference)
467
    {
468 132
        return null !== $reference && !$this->isLinkReference($reference);
469
    }
470
471
    /**
472
     * Turns a reference into a resource.
473
     *
474
     * @param string      $path      The Puli path.
475
     * @param string|null $reference The reference.
476
     *
477
     * @return PuliResource The resource.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use GenericResource.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
478
     */
479 72
    protected function createResource($path, $reference)
480
    {
481 72
        if (null === $reference) {
482 3
            $resource = new GenericResource();
483 72
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
484 4
            $resource = new LinkResource(substr($reference, 1));
485 70
        } elseif (is_dir($reference)) {
486 38
            $resource = new DirectoryResource($reference);
487 70
        } elseif (is_file($reference)) {
488 48
            $resource = new FileResource($reference);
489 48
        } else {
490
            throw new RuntimeException(sprintf(
491
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
492
                $reference
493
            ));
494
        }
495
496 72
        $resource->attachTo($this, $path);
497
498 72
        return $resource;
499
    }
500
501
    /**
502
     * Turns a list of references into a list of resources.
503
     *
504
     * The references are expected to be in the format returned by
505
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
506
     * {@link getReferencesInDirectory()}.
507
     *
508
     * The result contains Puli paths as keys and {@link PuliResource}
509
     * implementations as values. The order of the results is undefined.
510
     *
511
     * @param string[]|null[] $references The references indexed by Puli paths.
512
     *
513
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string|null|GenericResource>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
514
     */
515 28
    private function createResources(array $references)
516
    {
517 28
        foreach ($references as $path => $reference) {
518 20
            $references[$path] = $this->createResource($path, $reference);
0 ignored issues
show
Bug introduced by
It seems like $reference defined by $reference on line 517 can also be of type object<Puli\Repository\Resource\GenericResource>; however, Puli\Repository\Abstract...itory::createResource() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
519 28
        }
520
521 28
        return $references;
522
    }
523
524
    /**
525
     * Adds all ancestor directories of a path to the repository.
526
     *
527
     * @param string $path A Puli path.
528
     */
529 152
    private function ensureDirectoryExists($path)
530
    {
531 152
        if (array_key_exists($path, $this->json)) {
532 152
            return;
533
        }
534
535
        // Recursively initialize parent directories
536 48
        if ('/' !== $path) {
537 48
            $this->ensureDirectoryExists(Path::getDirectory($path));
538 48
        }
539
540 48
        $this->json[$path] = null;
541 48
    }
542
543
    /**
544
     * Adds a resource to the repository.
545
     *
546
     * @param string                          $path     The Puli path to add the
547
     *                                                  resource at.
548
     * @param FilesystemResource|LinkResource $resource The resource to add.
549
     */
550 152
    private function addResource($path, $resource)
551
    {
552 152
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
553 2
            throw new UnsupportedResourceException(sprintf(
554
                'The %s only supports adding FilesystemResource and '.
555 2
                'LinkedResource instances. Got: %s',
556
                // Get the short class name
557 2
                $this->getShortClassName(get_class($this)),
558 2
                $this->getShortClassName(get_class($resource))
559 2
            ));
560
        }
561
562
        // Don't modify resources attached to other repositories
563 150
        if ($resource->isAttached()) {
564 4
            $resource = clone $resource;
565 4
        }
566
567 150
        if ($resource instanceof LinkResource) {
568 4
            $resource->attachTo($this, $path);
569
570 4
            $this->insertReference($path, '@'.$resource->getTargetPath());
571
572 4
            $this->appendToChangeStream($resource);
573 4
        } else {
574
            // Extension point for the optimized repository
575 150
            $this->addFilesystemResource($path, $resource);
576
        }
577 150
    }
578
579
    /**
580
     * Returns the short name of a fully-qualified class name.
581
     *
582
     * @param string $className The fully-qualified class name.
583
     *
584
     * @return string The short class name.
585
     */
586 2
    private function getShortClassName($className)
587
    {
588 2
        if (false !== ($pos = strrpos($className, '\\'))) {
589 2
            return substr($className, $pos + 1);
590
        }
591
592 2
        return $className;
593
    }
594
}
595