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

AbstractJsonRepository   C

Complexity

Total Complexity 59

Size/Duplication

Total Lines 567
Duplicated Lines 1.94 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 96.81%

Importance

Changes 12
Bugs 4 Features 1
Metric Value
wmc 59
c 12
b 4
f 1
lcom 1
cbo 13
dl 11
loc 567
ccs 182
cts 188
cp 0.9681
rs 6.1905

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
B add() 11 26 4
A get() 0 16 3
A find() 0 14 2
A contains() 0 13 2
A remove() 0 17 2
A clear() 0 15 2
A listChildren() 0 21 4
B hasChildren() 0 22 4
insertReference() 0 1 ?
removeReferences() 0 1 ?
getReferencesForPath() 0 1 ?
getReferencesForGlob() 0 1 ?
getReferencesForRegex() 0 1 ?
getReferencesInDirectory() 0 1 ?
A addFilesystemResource() 0 8 1
B load() 0 26 6
B flush() 0 27 5
A isLinkReference() 0 4 2
A isFilesystemReference() 0 4 2
B createResource() 0 21 6
A createResources() 0 8 2
A ensureDirectoryExists() 0 13 3
B addResource() 0 28 5
A getShortClassName() 0 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractJsonRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractJsonRepository, and based on these observations, apply Extract Interface, too.

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 190
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
94
    {
95 190
        parent::__construct($changeStream);
96
97 190
        $this->baseDirectory = $baseDirectory;
98 190
        $this->path = Path::makeAbsolute($path, $baseDirectory);
99 190
        $this->encoder = new JsonEncoder();
100
101 190
        if ($validateJson) {
102 190
            $this->schemaPath = realpath(__DIR__.'/../res/schema/repository-schema-1.0.json');
103 190
        }
104 190
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 165
    public function add($path, $resource)
110 1
    {
111 165
        if (null === $this->json) {
112 165
            $this->load();
113 165
        }
114
115 165
        $path = $this->sanitizePath($path);
116
117 159 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 2
            $this->ensureDirectoryExists($path);
119
120 2
            foreach ($resource as $child) {
121 2
                $this->addResource($path.'/'.$child->getName(), $child);
122 2
            }
123
124 2
            $this->flush();
125
126 2
            return;
127
        }
128
129 157
        $this->ensureDirectoryExists(Path::getDirectory($path));
130
131 157
        $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 155
        $this->flush();
134 155
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 80
    public function get($path)
140
    {
141 80
        if (null === $this->json) {
142 21
            $this->load();
143 21
        }
144
145 80
        $path = $this->sanitizePath($path);
146 74
        $references = $this->getReferencesForPath($path);
147
148
        // Might be null, don't use isset()
149 74
        if (array_key_exists($path, $references)) {
150 70
            return $this->createResource($path, $references[$path]);
151
        }
152
153 4
        throw ResourceNotFoundException::forPath($path);
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 24
    public function find($query, $language = 'glob')
160
    {
161 24
        if (null === $this->json) {
162 2
            $this->load();
163 2
        }
164
165 24
        $this->validateSearchLanguage($language);
166 22
        $query = $this->sanitizePath($query);
167 16
        $results = $this->createResources($this->getReferencesForGlob($query));
168
169 16
        ksort($results);
170
171 16
        return new ArrayResourceCollection(array_values($results));
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 40
    public function contains($query, $language = 'glob')
178
    {
179 40
        if (null === $this->json) {
180 21
            $this->load();
181 21
        }
182
183 40
        $this->validateSearchLanguage($language);
184 38
        $query = $this->sanitizePath($query);
185
186 32
        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);
187
188 32
        return !empty($results);
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 26
    public function remove($query, $language = 'glob')
195
    {
196 26
        if (null === $this->json) {
197 11
            $this->load();
198 11
        }
199
200 26
        $this->validateSearchLanguage($language);
201 25
        $query = $this->sanitizePath($query);
202
203 19
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
204
205 15
        $removed = $this->removeReferences($query);
206
207 14
        $this->flush();
208
209 14
        return $removed;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215 76
    public function clear()
216
    {
217 2
        if (null === $this->json) {
218 76
            $this->load();
219
        }
220
221
        // Subtract root which is not deleted
222 2
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;
223
224 2
        $this->json = array();
225
226 2
        $this->flush();
227
228 2
        return $removed;
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234 27
    public function listChildren($path)
235
    {
236 27
        if (null === $this->json) {
237 3
            $this->load();
238 3
        }
239
240 27
        $path = $this->sanitizePath($path);
241 21
        $results = $this->createResources($this->getReferencesInDirectory($path));
242
243 21
        if (empty($results)) {
244 6
            $pathResults = $this->getReferencesForPath($path);
245
246 6
            if (empty($pathResults)) {
247 2
                throw ResourceNotFoundException::forPath($path);
248
            }
249 4
        }
250
251 19
        ksort($results);
252
253 19
        return new ArrayResourceCollection(array_values($results));
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 14
    public function hasChildren($path)
260
    {
261 14
        if (null === $this->json) {
262
            $this->load();
263
        }
264
265 14
        $path = $this->sanitizePath($path);
266
267 8
        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);
268
269 8
        if (empty($results)) {
270 4
            $pathResults = $this->getReferencesForPath($path);
271
272 4
            if (empty($pathResults)) {
273 2
                throw ResourceNotFoundException::forPath($path);
274
            }
275
276 2
            return false;
277
        }
278
279 6
        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 157
    protected function addFilesystemResource($path, FilesystemResource $resource)
390
    {
391 157
        $resource->attachTo($this, $path);
392
393 157
        $this->insertReference($path, $resource->getFilesystemPath());
394
395 157
        $this->appendToChangeStream($resource);
396 157
    }
397
398
    /**
399
     * Loads the JSON file.
400
     */
401 188
    protected function load()
402
    {
403 188
        $decoder = new JsonDecoder();
404
405 188
        $this->json = file_exists($this->path)
406 188
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
407 188
            : array();
408
409 188
        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 188
        if (!isset($this->json['/'])) {
421 188
            $this->json['/'] = null;
422 188
        }
423
424
        // Make sure the JSON is sorted in reverse order
425 188
        krsort($this->json);
426 188
    }
427
428
    /**
429
     * Writes the JSON file.
430
     */
431 157
    protected function flush()
432
    {
433
        // The root node always exists
434 157
        if (!isset($this->json['/'])) {
435 61
            $this->json['/'] = null;
436 61
        }
437
438
        // Always save in reverse order
439 157
        krsort($this->json);
440
441
        // Comply to schema
442 157
        $json = (object) $this->json;
443
444 157
        if (isset($json->{'_order'})) {
445 6
            $order = $json->{'_order'};
446
447 6
            foreach ($order as $path => $entries) {
448 6
                foreach ($entries as $key => $entry) {
449 6
                    $order[$path][$key] = (object) $entry;
450 6
                }
451 6
            }
452
453 6
            $json->{'_order'} = (object) $order;
454 6
        }
455
456 157
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
457 157
    }
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 135
    protected function isLinkReference($reference)
467
    {
468 135
        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 136
    protected function isFilesystemReference($reference)
480
    {
481 136
        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 82
    protected function createResource($path, $reference)
493
    {
494 82
        if (null === $reference) {
495 3
            $resource = new GenericResource();
496 82
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
497 4
            $resource = new LinkResource(substr($reference, 1));
498 80
        } elseif (is_dir($reference)) {
499 43
            $resource = new DirectoryResource($reference);
500 80
        } elseif (is_file($reference)) {
501 57
            $resource = new FileResource($reference);
502 57
        } 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 82
        $resource->attachTo($this, $path);
510
511 82
        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 37
    private function createResources(array $references)
529
    {
530 37
        foreach ($references as $path => $reference) {
531 29
            $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 37
        }
533
534 37
        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 159
    private function ensureDirectoryExists($path)
543
    {
544 159
        if (array_key_exists($path, $this->json)) {
545 159
            return;
546
        }
547
548
        // Recursively initialize parent directories
549 52
        if ('/' !== $path) {
550 52
            $this->ensureDirectoryExists(Path::getDirectory($path));
551 52
        }
552
553 52
        $this->json[$path] = null;
554 52
    }
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 159
    private function addResource($path, $resource)
564
    {
565 159
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
566 2
            throw new UnsupportedResourceException(sprintf(
567
                'The %s only supports adding FilesystemResource and '.
568 2
                'LinkedResource instances. Got: %s',
569
                // Get the short class name
570 2
                $this->getShortClassName(get_class($this)),
571 2
                $this->getShortClassName(get_class($resource))
572 2
            ));
573
        }
574
575
        // Don't modify resources attached to other repositories
576 157
        if ($resource->isAttached()) {
577 4
            $resource = clone $resource;
578 4
        }
579
580 157
        if ($resource instanceof LinkResource) {
581 4
            $resource->attachTo($this, $path);
582
583 4
            $this->insertReference($path, '@'.$resource->getTargetPath());
584
585 4
            $this->appendToChangeStream($resource);
586 4
        } else {
587
            // Extension point for the optimized repository
588 157
            $this->addFilesystemResource($path, $resource);
589
        }
590 157
    }
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 2
    private function getShortClassName($className)
600
    {
601 2
        if (false !== ($pos = strrpos($className, '\\'))) {
602 2
            return substr($className, $pos + 1);
603
        }
604
605 2
        return $className;
606
    }
607
}
608