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

AbstractJsonRepository   C

Complexity

Total Complexity 59

Size/Duplication

Total Lines 559
Duplicated Lines 1.97 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 96.28%

Importance

Changes 11
Bugs 4 Features 1
Metric Value
wmc 59
c 11
b 4
f 1
lcom 1
cbo 13
dl 11
loc 559
ccs 181
cts 188
cp 0.9628
rs 6.1905

25 Methods

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