Failed Conditions
Pull Request — 1.0 (#79)
by Bernhard
02:54
created

AbstractJsonRepository::insertReference()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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