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

AbstractJsonRepository::getReferencesInDirectory()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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