Failed Conditions
Push — 1.0 ( 9f5a0b...fe7a2f )
by Bernhard
30:36 queued 17:00
created

src/AbstractJsonRepository.php (1 issue)

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