Failed Conditions
Push — 1.0 ( 8ca4b1...5bdf31 )
by Bernhard
08:51
created

AbstractJsonRepository::getShortClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4286
cc 2
eloc 4
nc 2
nop 1
crap 2
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 Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\LogLevel;
17
use Puli\Repository\Api\ChangeStream\ChangeStream;
18
use Puli\Repository\Api\Resource\FilesystemResource;
19
use Puli\Repository\Api\Resource\PuliResource;
20
use Puli\Repository\Api\ResourceCollection;
21
use Puli\Repository\Api\ResourceNotFoundException;
22
use Puli\Repository\Api\UnsupportedResourceException;
23
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
24
use Puli\Repository\Resource\DirectoryResource;
25
use Puli\Repository\Resource\FileResource;
26
use Puli\Repository\Resource\GenericResource;
27
use Puli\Repository\Resource\LinkResource;
28
use RuntimeException;
29
use Webmozart\Assert\Assert;
30
use Webmozart\Json\JsonDecoder;
31
use Webmozart\Json\JsonEncoder;
32
use Webmozart\PathUtil\Path;
33
34
/**
35
 * Base class for repositories backed by a JSON file.
36
 *
37
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
38
 *
39
 * @since  1.0
40
 *
41
 * @author Bernhard Schussek <[email protected]>
42
 * @author Titouan Galopin <[email protected]>
43
 */
44
abstract class AbstractJsonRepository extends AbstractEditableRepository implements LoggerAwareInterface
45
{
46
    /**
47
     * Flag: Whether to stop after the first result.
48
     *
49
     * @internal
50
     */
51
    const STOP_ON_FIRST = 1;
52
53
    /**
54
     * @var array
55
     */
56
    protected $json;
57
58
    /**
59
     * @var string
60
     */
61
    protected $baseDirectory;
62
63
    /**
64
     * @var string
65
     */
66
    private $path;
67
68
    /**
69
     * @var string
70
     */
71
    private $schemaPath;
72
73
    /**
74
     * @var JsonEncoder
75
     */
76
    private $encoder;
77
78
    /**
79
     * @var LoggerInterface
80
     */
81
    private $logger;
82
83
    /**
84
     * Creates a new repository.
85
     *
86
     * @param string            $path          The path to the JSON file. If
87
     *                                         relative, it must be relative to
88
     *                                         the base directory.
89
     * @param string            $baseDirectory The base directory of the store.
90
     *                                         Paths inside that directory are
91
     *                                         stored as relative paths. Paths
92
     *                                         outside that directory are stored
93
     *                                         as absolute paths.
94
     * @param bool              $validateJson  Whether to validate the JSON file
95
     *                                         against the schema. Slow but
96
     *                                         spots problems.
97
     * @param ChangeStream|null $changeStream  If provided, the repository will
98
     *                                         append resource changes to this
99
     *                                         change stream.
100
     */
101 392
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
102
    {
103 392
        parent::__construct($changeStream);
104
105 392
        $this->baseDirectory = $baseDirectory;
106 392
        $this->path = Path::makeAbsolute($path, $baseDirectory);
107 392
        $this->encoder = new JsonEncoder();
108 392
        $this->encoder->setPrettyPrinting(true);
109 392
        $this->encoder->setEscapeSlash(false);
110
111 392
        if ($validateJson) {
112 392
            $this->schemaPath = realpath(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
113 392
        }
114 392
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 22
    public function setLogger(LoggerInterface $logger = null)
120
    {
121 22
        $this->logger = $logger;
122 22
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 352
    public function add($path, $resource)
128
    {
129 352
        if (null === $this->json) {
130 352
            $this->load();
131 352
        }
132
133 352
        $path = $this->sanitizePath($path);
134
135 340 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...
136 4
            $this->ensureDirectoryExists($path);
137
138 4
            foreach ($resource as $child) {
139 4
                $this->addResource($path.'/'.$child->getName(), $child);
140 4
            }
141
142 4
            $this->flush();
143
144 4
            return;
145
        }
146
147 336
        $this->ensureDirectoryExists(Path::getDirectory($path));
148
149 336
        $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...
150
151 332
        $this->flush();
152 332
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 156
    public function get($path)
158
    {
159 156
        if (null === $this->json) {
160 24
            $this->load();
161 24
        }
162
163 156
        $path = $this->sanitizePath($path);
164 144
        $references = $this->getReferencesForPath($path);
165
166
        // Might be null, don't use isset()
167 144
        if (array_key_exists($path, $references)) {
168 134
            return $this->createResource($path, $references[$path]);
169
        }
170
171 10
        throw ResourceNotFoundException::forPath($path);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 48
    public function find($query, $language = 'glob')
178
    {
179 48
        if (null === $this->json) {
180 6
            $this->load();
181 6
        }
182
183 48
        $this->failUnlessGlob($language);
184 44
        $query = $this->sanitizePath($query);
185 32
        $results = $this->createResources($this->getReferencesForGlob($query));
186
187 32
        ksort($results);
188
189 32
        return new ArrayResourceCollection(array_values($results));
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 68
    public function contains($query, $language = 'glob')
196
    {
197 68
        if (null === $this->json) {
198 22
            $this->load();
199 22
        }
200
201 68
        $this->failUnlessGlob($language);
202 64
        $query = $this->sanitizePath($query);
203
204 52
        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);
205
206 52
        return !empty($results);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 66
    public function remove($query, $language = 'glob')
213
    {
214 66
        if (null === $this->json) {
215 24
            $this->load();
216 24
        }
217
218 66
        $this->failUnlessGlob($language);
219 62
        $query = $this->sanitizePath($query);
220
221 50
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
222
223 42
        $removed = $this->removeReferences($query);
224
225 40
        $this->flush();
226
227 40
        return $removed;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 172
    public function clear()
234
    {
235 6
        if (null === $this->json) {
236 2
            $this->load();
237 2
        }
238
239
        // Subtract root which is not deleted
240 6
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;
241
242 6
        $this->json = array();
243
244 6
        $this->flush();
245
246 172
        return $removed;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 48
    public function listChildren($path)
253
    {
254 48
        if (null === $this->json) {
255 2
            $this->load();
256 2
        }
257
258 48
        $path = $this->sanitizePath($path);
259 36
        $results = $this->createResources($this->getReferencesInDirectory($path));
260
261 36
        if (empty($results)) {
262 16
            $pathResults = $this->getReferencesForPath($path);
263
264 16
            if (empty($pathResults)) {
265 4
                throw ResourceNotFoundException::forPath($path);
266
            }
267 12
        }
268
269 32
        ksort($results);
270
271 32
        return new ArrayResourceCollection(array_values($results));
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 32
    public function hasChildren($path)
278
    {
279 32
        if (null === $this->json) {
280 2
            $this->load();
281 2
        }
282
283 32
        $path = $this->sanitizePath($path);
284
285 20
        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);
286
287 20
        if (empty($results)) {
288 12
            $pathResults = $this->getReferencesForPath($path);
289
290 12
            if (empty($pathResults)) {
291 4
                throw ResourceNotFoundException::forPath($path);
292
            }
293
294 8
            return false;
295
        }
296
297 12
        return true;
298
    }
299
300
    /**
301
     * Inserts a path reference into the JSON file.
302
     *
303
     * The path reference can be:
304
     *
305
     *  * a link starting with `@`
306
     *  * an absolute filesystem path
307
     *
308
     * @param string      $path      The Puli path.
309
     * @param string|null $reference The path reference.
310
     */
311
    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...
312
313
    /**
314
     * Removes all path references matching the given glob from the JSON file.
315
     *
316
     * @param string $glob The glob for a list of Puli paths.
317
     */
318
    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...
319
320
    /**
321
     * Returns the references for a given Puli path.
322
     *
323
     * Each reference returned by this method can be:
324
     *
325
     *  * `null`
326
     *  * a link starting with `@`
327
     *  * an absolute filesystem path
328
     *
329
     * The result has either one entry or none, if no path was found. The key
330
     * of the single entry is the path passed to this method.
331
     *
332
     * @param string $path The Puli path.
333
     *
334
     * @return string[]|null[] A one-level array of references with Puli paths
335
     *                         as keys. The array has at most one entry.
336
     */
337
    abstract protected function getReferencesForPath($path);
338
339
    /**
340
     * Returns the references matching a given Puli path glob.
341
     *
342
     * Each reference returned by this method can be:
343
     *
344
     *  * `null`
345
     *  * a link starting with `@`
346
     *  * an absolute filesystem path
347
     *
348
     * The keys of the returned array are Puli paths. Their order is undefined.
349
     *
350
     * @param string $glob  The glob.
351
     * @param int    $flags A bitwise combination of the flag constants in this
352
     *                      class.
353
     *
354
     * @return string[]|null[] A one-level array of references with Puli paths
355
     *                         as keys.
356
     */
357
    abstract protected function getReferencesForGlob($glob, $flags = 0);
358
359
    /**
360
     * Returns the references matching a given Puli path regular expression.
361
     *
362
     * Each reference returned by this method can be:
363
     *
364
     *  * `null`
365
     *  * a link starting with `@`
366
     *  * an absolute filesystem path
367
     *
368
     * The keys of the returned array are Puli paths. Their order is undefined.
369
     *
370
     * @param string $staticPrefix The static prefix of all Puli paths matching
371
     *                             the regular expression.
372
     * @param string $regex        The regular expression.
373
     * @param int    $flags        A bitwise combination of the flag constants
374
     *                             in this class.
375
     *
376
     * @return string[]|null[] A one-level array of references with Puli paths
377
     *                         as keys.
378
     */
379
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);
380
381
    /**
382
     * Returns the references in a given Puli path.
383
     *
384
     * Each reference returned by this method can be:
385
     *
386
     *  * `null`
387
     *  * a link starting with `@`
388
     *  * an absolute filesystem path
389
     *
390
     * The keys of the returned array are Puli paths. Their order is undefined.
391
     *
392
     * @param string $path  The Puli path.
393
     * @param int    $flags A bitwise combination of the flag constants in this
394
     *                      class.
395
     *
396
     * @return string[]|null[] A one-level array of references with Puli paths
397
     *                         as keys.
398
     */
399
    abstract protected function getReferencesInDirectory($path, $flags = 0);
400
401
    /**
402
     * Logs a message.
403
     *
404
     * @param mixed  $level   One of the level constants in {@link LogLevel}.
405
     * @param string $message The message.
406
     */
407 22
    protected function log($level, $message)
408
    {
409 22
        if (null !== $this->logger) {
410 22
            $this->logger->log($level, $message);
411 22
        }
412 22
    }
413
414
    /**
415
     * Logs a warning that a reference could not be found.
416
     *
417
     * @param string $path              The Puli path of a path mapping.
418
     * @param string $reference         The reference that was not found.
419
     * @param string $absoluteReference The absolute filesystem path of the
420
     *                                  reference.
421
     */
422 22
    protected function logReferenceNotFound($path, $reference, $absoluteReference)
423
    {
424 22
        $this->log(LogLevel::WARNING, sprintf(
425 22
            'The reference "%s"%s mapped by the path %s could not be found.',
426 22
            $reference,
427 22
            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',
428
            $path
429 22
        ));
430 22
    }
431
432
    /**
433
     * Adds a filesystem resource to the JSON file.
434
     *
435
     * @param string             $path     The Puli path.
436
     * @param FilesystemResource $resource The resource to add.
437
     */
438 336
    protected function addFilesystemResource($path, FilesystemResource $resource)
439
    {
440 336
        $resource->attachTo($this, $path);
441
442 336
        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);
443
444 336
        $this->insertReference($path, $relativePath);
445
446 336
        $this->storeVersion($resource);
447 336
    }
448
449
    /**
450
     * Loads the JSON file.
451
     */
452 392
    protected function load()
453
    {
454 392
        $decoder = new JsonDecoder();
455
456 392
        $this->json = file_exists($this->path)
457 392
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
458 392
            : array();
459
460 392
        if (isset($this->json['_order'])) {
461 5
            $this->json['_order'] = (array) $this->json['_order'];
462
463 5
            foreach ($this->json['_order'] as $path => $entries) {
464 5
                foreach ($entries as $key => $entry) {
465 5
                    $this->json['_order'][$path][$key] = (array) $entry;
466 5
                }
467 5
            }
468 5
        }
469
470
        // The root node always exists
471 392
        if (!isset($this->json['/'])) {
472 392
            $this->json['/'] = null;
473 392
        }
474
475
        // Make sure the JSON is sorted in reverse order
476 392
        krsort($this->json);
477 392
    }
478
479
    /**
480
     * Writes the JSON file.
481
     */
482 340
    protected function flush()
483
    {
484
        // The root node always exists
485 340
        if (!isset($this->json['/'])) {
486 144
            $this->json['/'] = null;
487 144
        }
488
489
        // Always save in reverse order
490 340
        krsort($this->json);
491
492
        // Comply to schema
493 340
        $json = (object) $this->json;
494
495 340
        if (isset($json->{'_order'})) {
496 10
            $order = $json->{'_order'};
497
498 10
            foreach ($order as $path => $entries) {
499 10
                foreach ($entries as $key => $entry) {
500 10
                    $order[$path][$key] = (object) $entry;
501 10
                }
502 10
            }
503
504 10
            $json->{'_order'} = (object) $order;
505 10
        }
506
507 340
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
508 340
    }
509
510
    /**
511
     * Returns whether a reference contains a link.
512
     *
513
     * @param string $reference The reference.
514
     *
515
     * @return bool Whether the reference contains a link.
516
     */
517 280
    protected function isLinkReference($reference)
518
    {
519 280
        return isset($reference{0}) && '@' === $reference{0};
520
    }
521
522
    /**
523
     * Returns whether a reference contains an absolute or relative filesystem
524
     * path.
525
     *
526
     * @param string $reference The reference.
527
     *
528
     * @return bool Whether the reference contains a filesystem path.
529
     */
530 286
    protected function isFilesystemReference($reference)
531
    {
532 286
        return null !== $reference && !$this->isLinkReference($reference);
533
    }
534
535
    /**
536
     * Turns a reference into a resource.
537
     *
538
     * @param string      $path      The Puli path.
539
     * @param string|null $reference The reference.
540
     *
541
     * @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...
542
     */
543 152
    protected function createResource($path, $reference)
544
    {
545 152
        if (null === $reference) {
546 12
            $resource = new GenericResource();
547 152
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
548 8
            $resource = new LinkResource(substr($reference, 1));
549 140
        } elseif (is_dir($reference)) {
550 76
            $resource = new DirectoryResource($reference);
551 140
        } elseif (is_file($reference)) {
552 92
            $resource = new FileResource($reference);
553 92
        } else {
554
            throw new RuntimeException(sprintf(
555
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
556
                $reference
557
            ));
558
        }
559
560 152
        $resource->attachTo($this, $path);
561
562 152
        return $resource;
563
    }
564
565
    /**
566
     * Turns a list of references into a list of resources.
567
     *
568
     * The references are expected to be in the format returned by
569
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
570
     * {@link getReferencesInDirectory()}.
571
     *
572
     * The result contains Puli paths as keys and {@link PuliResource}
573
     * implementations as values. The order of the results is undefined.
574
     *
575
     * @param string[]|null[] $references The references indexed by Puli paths.
576
     *
577
     * @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...
578
     */
579 68
    private function createResources(array $references)
580
    {
581 68
        foreach ($references as $path => $reference) {
582 44
            $references[$path] = $this->createResource($path, $reference);
0 ignored issues
show
Bug introduced by
It seems like $reference defined by $reference on line 581 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...
583 68
        }
584
585 68
        return $references;
586
    }
587
588
    /**
589
     * Adds all ancestor directories of a path to the repository.
590
     *
591
     * @param string $path A Puli path.
592
     */
593 340
    private function ensureDirectoryExists($path)
594
    {
595 340
        if (array_key_exists($path, $this->json)) {
596 340
            return;
597
        }
598
599
        // Recursively initialize parent directories
600 112
        if ('/' !== $path) {
601 112
            $this->ensureDirectoryExists(Path::getDirectory($path));
602 112
        }
603
604 112
        $this->json[$path] = null;
605 112
    }
606
607
    /**
608
     * Adds a resource to the repository.
609
     *
610
     * @param string                          $path     The Puli path to add the
611
     *                                                  resource at.
612
     * @param FilesystemResource|LinkResource $resource The resource to add.
613
     */
614 340
    private function addResource($path, $resource)
615
    {
616 340
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
617 4
            throw new UnsupportedResourceException(sprintf(
618
                'The %s only supports adding FilesystemResource and '.
619 4
                'LinkedResource instances. Got: %s',
620
                // Get the short class name
621 4
                $this->getShortClassName(get_class($this)),
622 4
                $this->getShortClassName(get_class($resource))
623 4
            ));
624
        }
625
626
        // Don't modify resources attached to other repositories
627 336
        if ($resource->isAttached()) {
628 4
            $resource = clone $resource;
629 4
        }
630
631 336
        if ($resource instanceof LinkResource) {
632 8
            $resource->attachTo($this, $path);
633
634 8
            $this->insertReference($path, '@'.$resource->getTargetPath());
635
636 8
            $this->storeVersion($resource);
637 8
        } else {
638
            // Extension point for the optimized repository
639 336
            $this->addFilesystemResource($path, $resource);
640
        }
641 336
    }
642
643
    /**
644
     * Returns the short name of a fully-qualified class name.
645
     *
646
     * @param string $className The fully-qualified class name.
647
     *
648
     * @return string The short class name.
649
     */
650 4
    private function getShortClassName($className)
651
    {
652 4
        if (false !== ($pos = strrpos($className, '\\'))) {
653 4
            return substr($className, $pos + 1);
654
        }
655
656 4
        return $className;
657
    }
658
}
659