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