Completed
Push — 1.0 ( 7c2492...85ea96 )
by Bernhard
04:58
created

src/AbstractJsonRepository.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 = Path::canonicalize(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
113
        }
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
        }
132
133 352
        $path = $this->sanitizePath($path);
134
135 340 View Code Duplication
        if ($resource instanceof ResourceCollection) {
136 4
            $this->ensureDirectoryExists($path);
137
138 4
            foreach ($resource as $child) {
139 4
                $this->addResource($path.'/'.$child->getName(), $child);
140
            }
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);
150
151 332
        $this->flush();
152 332
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 164
    public function get($path)
158
    {
159 164
        if (null === $this->json) {
160 28
            $this->load();
161
        }
162
163 164
        $path = $this->sanitizePath($path);
164 152
        $references = $this->getReferencesForPath($path);
165
166
        // Might be null, don't use isset()
167 152
        if (array_key_exists($path, $references)) {
168 142
            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
        }
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
        }
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
        }
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 6
    public function clear()
234
    {
235 6
        if (null === $this->json) {
236 2
            $this->load();
237
        }
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 6
        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
        }
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
        }
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
        }
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);
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);
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
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
351
     *
352
     * @param string $glob  The glob.
353
     * @param int    $flags A bitwise combination of the flag constants in this
354
     *                      class.
355
     *
356
     * @return string[]|null[] A one-level array of references with Puli paths
357
     *                         as keys.
358
     */
359
    abstract protected function getReferencesForGlob($glob, $flags = 0);
360
361
    /**
362
     * Returns the references matching a given Puli path regular expression.
363
     *
364
     * Each reference returned by this method can be:
365
     *
366
     *  * `null`
367
     *  * a link starting with `@`
368
     *  * an absolute filesystem path
369
     *
370
     * The keys of the returned array are Puli paths. Their order is undefined.
371
     *
372
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
373
     *
374
     * @param string $staticPrefix The static prefix of all Puli paths matching
375
     *                             the regular expression.
376
     * @param string $regex        The regular expression.
377
     * @param int    $flags        A bitwise combination of the flag constants
378
     *                             in this class.
379
     *
380
     * @return string[]|null[] A one-level array of references with Puli paths
381
     *                         as keys.
382
     */
383
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);
384
385
    /**
386
     * Returns the references in a given Puli path.
387
     *
388
     * Each reference returned by this method can be:
389
     *
390
     *  * `null`
391
     *  * a link starting with `@`
392
     *  * an absolute filesystem path
393
     *
394
     * The keys of the returned array are Puli paths. Their order is undefined.
395
     *
396
     * The flag `STOP_ON_FIRST` may be used to stop the search at the first result.
397
     *
398
     * @param string $path  The Puli path.
399
     * @param int    $flags A bitwise combination of the flag constants in this
400
     *                      class.
401
     *
402
     * @return string[]|null[] A one-level array of references with Puli paths
403
     *                         as keys.
404
     */
405
    abstract protected function getReferencesInDirectory($path, $flags = 0);
406
407
    /**
408
     * Logs a message.
409
     *
410
     * @param mixed  $level   One of the level constants in {@link LogLevel}.
411
     * @param string $message The message.
412
     */
413 22
    protected function log($level, $message)
414
    {
415 22
        if (null !== $this->logger) {
416 22
            $this->logger->log($level, $message);
417
        }
418 22
    }
419
420
    /**
421
     * Logs a warning that a reference could not be found.
422
     *
423
     * @param string $path              The Puli path of a path mapping.
424
     * @param string $reference         The reference that was not found.
425
     * @param string $absoluteReference The absolute filesystem path of the
426
     *                                  reference.
427
     */
428 22
    protected function logReferenceNotFound($path, $reference, $absoluteReference)
429
    {
430 22
        $this->log(LogLevel::WARNING, sprintf(
431 22
            'The reference "%s"%s mapped by the path %s could not be found.',
432
            $reference,
433 22
            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',
434
            $path
435
        ));
436 22
    }
437
438
    /**
439
     * Adds a filesystem resource to the JSON file.
440
     *
441
     * @param string             $path     The Puli path.
442
     * @param FilesystemResource $resource The resource to add.
443
     */
444 336
    protected function addFilesystemResource($path, FilesystemResource $resource)
445
    {
446 336
        $resource = clone $resource;
447 336
        $resource->attachTo($this, $path);
448
449 336
        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);
450
451 336
        $this->insertReference($path, $relativePath);
452
453 336
        $this->storeVersion($resource);
454 336
    }
455
456
    /**
457
     * Loads the JSON file.
458
     */
459 392
    protected function load()
460
    {
461 392
        $decoder = new JsonDecoder();
462
463 392
        $this->json = file_exists($this->path)
464 69
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
465 392
            : array();
466
467 392
        if (isset($this->json['_order'])) {
468 5
            $this->json['_order'] = (array) $this->json['_order'];
469
470 5
            foreach ($this->json['_order'] as $path => $entries) {
471 5
                foreach ($entries as $key => $entry) {
472 5
                    $this->json['_order'][$path][$key] = (array) $entry;
473
                }
474
            }
475
        }
476
477
        // The root node always exists
478 392
        if (!isset($this->json['/'])) {
479 392
            $this->json['/'] = null;
480
        }
481
482
        // Make sure the JSON is sorted in reverse order
483 392
        krsort($this->json);
484 392
    }
485
486
    /**
487
     * Writes the JSON file.
488
     */
489 340
    protected function flush()
490
    {
491
        // The root node always exists
492 340
        if (!isset($this->json['/'])) {
493 144
            $this->json['/'] = null;
494
        }
495
496
        // Always save in reverse order
497 340
        krsort($this->json);
498
499
        // Comply to schema
500 340
        $json = (object) $this->json;
501
502 340
        if (isset($json->{'_order'})) {
503 10
            $order = $json->{'_order'};
504
505 10
            foreach ($order as $path => $entries) {
506 10
                foreach ($entries as $key => $entry) {
507 10
                    $order[$path][$key] = (object) $entry;
508
                }
509
            }
510
511 10
            $json->{'_order'} = (object) $order;
512
        }
513
514 340
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
515 340
    }
516
517
    /**
518
     * Returns whether a reference contains a link.
519
     *
520
     * @param string $reference The reference.
521
     *
522
     * @return bool Whether the reference contains a link.
523
     */
524 284
    protected function isLinkReference($reference)
525
    {
526 284
        return isset($reference{0}) && '@' === $reference{0};
527
    }
528
529
    /**
530
     * Returns whether a reference contains an absolute or relative filesystem
531
     * path.
532
     *
533
     * @param string $reference The reference.
534
     *
535
     * @return bool Whether the reference contains a filesystem path.
536
     */
537 290
    protected function isFilesystemReference($reference)
538
    {
539 290
        return null !== $reference && !$this->isLinkReference($reference);
540
    }
541
542
    /**
543
     * Turns a reference into a resource.
544
     *
545
     * @param string      $path      The Puli path.
546
     * @param string|null $reference The reference.
547
     *
548
     * @return PuliResource The resource.
549
     */
550 160
    protected function createResource($path, $reference)
551
    {
552 160
        if (null === $reference) {
553 12
            $resource = new GenericResource();
554 148
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
555 8
            $resource = new LinkResource(substr($reference, 1));
556 148
        } elseif (is_dir($reference)) {
557 84
            $resource = new DirectoryResource($reference);
558 100
        } elseif (is_file($reference)) {
559 100
            $resource = new FileResource($reference);
560
        } else {
561
            throw new RuntimeException(sprintf(
562
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
563
                $reference
564
            ));
565
        }
566
567 160
        $resource->attachTo($this, $path);
568
569 160
        return $resource;
570
    }
571
572
    /**
573
     * Turns a list of references into a list of resources.
574
     *
575
     * The references are expected to be in the format returned by
576
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
577
     * {@link getReferencesInDirectory()}.
578
     *
579
     * The result contains Puli paths as keys and {@link PuliResource}
580
     * implementations as values. The order of the results is undefined.
581
     *
582
     * @param string[]|null[] $references The references indexed by Puli paths.
583
     *
584
     * @return array
585
     */
586 68
    private function createResources(array $references)
587
    {
588 68
        foreach ($references as $path => $reference) {
589 44
            $references[$path] = $this->createResource($path, $reference);
0 ignored issues
show
It seems like $reference defined by $reference on line 588 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...
590
        }
591
592 68
        return $references;
593
    }
594
595
    /**
596
     * Adds all ancestor directories of a path to the repository.
597
     *
598
     * @param string $path A Puli path.
599
     */
600 340
    private function ensureDirectoryExists($path)
601
    {
602 340
        if (array_key_exists($path, $this->json)) {
603 340
            return;
604
        }
605
606
        // Recursively initialize parent directories
607 104
        if ('/' !== $path) {
608 104
            $this->ensureDirectoryExists(Path::getDirectory($path));
609
        }
610
611 104
        $this->json[$path] = null;
612 104
    }
613
614
    /**
615
     * Adds a resource to the repository.
616
     *
617
     * @param string                          $path     The Puli path to add the
618
     *                                                  resource at.
619
     * @param FilesystemResource|LinkResource $resource The resource to add.
620
     */
621 340
    private function addResource($path, $resource)
622
    {
623 340
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
624 4
            throw new UnsupportedResourceException(sprintf(
625
                'The %s only supports adding FilesystemResource and '.
626 4
                'LinkedResource instances. Got: %s',
627
                // Get the short class name
628 4
                $this->getShortClassName(get_class($this)),
629 4
                $this->getShortClassName(get_class($resource))
630
            ));
631
        }
632
633 336
        if ($resource instanceof LinkResource) {
634 8
            $resource = clone $resource;
635 8
            $resource->attachTo($this, $path);
636
637 8
            $this->insertReference($path, '@'.$resource->getTargetPath());
638
639 8
            $this->storeVersion($resource);
640
        } else {
641
            // Extension point for the optimized repository
642 336
            $this->addFilesystemResource($path, $resource);
643
        }
644 336
    }
645
646
    /**
647
     * Returns the short name of a fully-qualified class name.
648
     *
649
     * @param string $className The fully-qualified class name.
650
     *
651
     * @return string The short class name.
652
     */
653 4
    private function getShortClassName($className)
654
    {
655 4
        if (false !== ($pos = strrpos($className, '\\'))) {
656 4
            return substr($className, $pos + 1);
657
        }
658
659 4
        return $className;
660
    }
661
}
662