Failed Conditions
Pull Request — 1.0 (#79)
by Bernhard
04:43
created

AbstractJsonRepository::flush()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

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