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 JsonRepository 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 JsonRepository, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
70 | class JsonRepository extends AbstractJsonRepository implements EditableRepository |
||
71 | { |
||
72 | /** |
||
73 | * Flag: Don't search the contents of mapped directories for matching paths. |
||
74 | * |
||
75 | * @internal |
||
76 | */ |
||
77 | const NO_SEARCH_FILESYSTEM = 2; |
||
78 | |||
79 | /** |
||
80 | * Flag: Don't filter out references that don't exist on the filesystem. |
||
81 | * |
||
82 | * @internal |
||
83 | */ |
||
84 | const NO_CHECK_FILE_EXISTS = 4; |
||
85 | |||
86 | /** |
||
87 | * Flag: Include the references for mapped ancestor paths /a of a path /a/b. |
||
88 | * |
||
89 | * @internal |
||
90 | */ |
||
91 | const INCLUDE_ANCESTORS = 8; |
||
92 | |||
93 | /** |
||
94 | * Flag: Include the references for mapped nested paths /a/b of a path /a. |
||
95 | * |
||
96 | * @internal |
||
97 | */ |
||
98 | const INCLUDE_NESTED = 16; |
||
99 | |||
100 | /** |
||
101 | * Creates a new repository. |
||
102 | * |
||
103 | * @param string $path The path to the JSON file. If relative, it |
||
104 | * must be relative to the base directory. |
||
105 | * @param string $baseDirectory The base directory of the store. Paths |
||
106 | * inside that directory are stored as relative |
||
107 | * paths. Paths outside that directory are |
||
108 | * stored as absolute paths. |
||
109 | * @param bool $validateJson Whether to validate the JSON file against |
||
110 | * the schema. Slow but spots problems. |
||
111 | */ |
||
112 | 198 | public function __construct($path, $baseDirectory, $validateJson = false) |
|
118 | |||
119 | /** |
||
120 | * {@inheritdoc} |
||
121 | */ |
||
122 | 32 | public function getVersions($path) |
|
123 | { |
||
124 | 32 | if (!$this->json) { |
|
|
|||
125 | 13 | $this->load(); |
|
126 | } |
||
127 | |||
128 | 32 | $references = $this->searchReferences($path); |
|
129 | |||
130 | 32 | if (!isset($references[$path])) { |
|
131 | 10 | throw NoVersionFoundException::forPath($path); |
|
132 | } |
||
133 | |||
134 | 22 | $resources = array(); |
|
135 | 22 | $pathReferences = $references[$path]; |
|
136 | |||
137 | // The first reference is the last (current) version |
||
138 | // Hence traverse in reverse order |
||
139 | 22 | for ($ref = end($pathReferences); null !== key($pathReferences); $ref = prev($pathReferences)) { |
|
140 | 22 | $resources[] = $this->createResource($path, $ref); |
|
141 | } |
||
142 | |||
143 | 22 | return new VersionList($path, $resources); |
|
144 | } |
||
145 | |||
146 | /** |
||
147 | * {@inheritdoc} |
||
148 | */ |
||
149 | 170 | protected function storeVersion(PuliResource $resource) |
|
150 | { |
||
151 | 170 | $path = $resource->getPath(); |
|
152 | |||
153 | // Newly inserted parent directories and the resource need to be |
||
154 | // sorted before we can correctly search references below |
||
155 | 170 | krsort($this->json); |
|
156 | |||
157 | // If a mapping exists for a sub-path of this resource |
||
158 | // (e.g. $path = /a, mapped sub-path = /a/b) |
||
159 | // we need to record the order, since by default sub-paths are |
||
160 | // preferred over super paths |
||
161 | |||
162 | 170 | $references = $this->searchReferences( |
|
163 | $path, |
||
164 | // Don't do filesystem checks here. We only check the filesystem |
||
165 | // when reading, not when adding. |
||
166 | 170 | self::NO_SEARCH_FILESYSTEM | self::NO_CHECK_FILE_EXISTS |
|
167 | // Include references for mapped ancestor and nested paths |
||
168 | 170 | | self::INCLUDE_ANCESTORS | self::INCLUDE_NESTED |
|
169 | ); |
||
170 | |||
171 | // Filter virtual resources |
||
172 | 170 | $references = array_filter($references, function ($currentReferences) { |
|
173 | 170 | return array(null) !== $currentReferences; |
|
174 | 170 | }); |
|
175 | |||
176 | // The $references contain: |
||
177 | // - any sub references (e.g. /a/b/c, /a/b/d) |
||
178 | // - the reference itself at $pos (e.g. /a/b) |
||
179 | // - non-null parent references (e.g. /a) |
||
180 | // (in that order, since long paths are sorted before short paths) |
||
181 | 170 | $pos = array_search($path, array_keys($references), true); |
|
182 | |||
183 | // We need to do three things: |
||
184 | |||
185 | // 1. If any parent mapping has an order defined, inherit that order |
||
186 | |||
187 | 170 | if ($pos + 1 < count($references)) { |
|
188 | // Inherit the parent order if necessary |
||
189 | 14 | if (!isset($this->json['_order'][$path])) { |
|
190 | 14 | $parentReferences = array_slice($references, $pos + 1); |
|
191 | |||
192 | 14 | $this->initWithParentOrder($path, $parentReferences); |
|
193 | } |
||
194 | |||
195 | // A parent order was inherited. Insert the path itself. |
||
196 | 14 | if (isset($this->json['_order'][$path])) { |
|
197 | 2 | $this->prependOrderEntry($path, $path); |
|
198 | } |
||
199 | } |
||
200 | |||
201 | // 2. If there are child mappings, insert the current path into their order |
||
202 | |||
203 | 170 | if ($pos > 0) { |
|
204 | 10 | $subReferences = array_slice($references, 0, $pos); |
|
205 | |||
206 | 10 | foreach ($subReferences as $subPath => $_) { |
|
207 | 10 | if (isset($this->json['_order'][$subPath])) { |
|
208 | 6 | continue; |
|
209 | } |
||
210 | |||
211 | 10 | if (isset($this->json['_order'][$path])) { |
|
212 | $this->json['_order'][$subPath] = $this->json['_order'][$path]; |
||
213 | } else { |
||
214 | 10 | $this->initWithDefaultOrder($subPath, $path, $references); |
|
215 | } |
||
216 | } |
||
217 | |||
218 | // After initializing all order entries, insert the new one |
||
219 | 10 | foreach ($subReferences as $subPath => $_) { |
|
220 | 10 | $this->prependOrderEntry($subPath, $path); |
|
221 | } |
||
222 | } |
||
223 | 170 | } |
|
224 | |||
225 | /** |
||
226 | * {@inheritdoc} |
||
227 | */ |
||
228 | 170 | protected function insertReference($path, $reference) |
|
229 | { |
||
230 | 170 | if (!isset($this->json[$path])) { |
|
231 | // Store first entries as simple reference |
||
232 | 170 | $this->json[$path] = $reference; |
|
233 | |||
234 | 170 | return; |
|
235 | } |
||
236 | |||
237 | 14 | if ($reference === $this->json[$path]) { |
|
238 | // Reference is already set |
||
239 | 4 | return; |
|
240 | } |
||
241 | |||
242 | 10 | if (!is_array($this->json[$path])) { |
|
243 | // Convert existing entries to arrays for follow ups |
||
244 | 10 | $this->json[$path] = array($this->json[$path]); |
|
245 | } |
||
246 | |||
247 | 10 | if (!in_array($reference, $this->json[$path], true)) { |
|
248 | // Insert at the beginning of the array |
||
249 | 10 | array_unshift($this->json[$path], $reference); |
|
250 | } |
||
251 | 10 | } |
|
252 | |||
253 | /** |
||
254 | * {@inheritdoc} |
||
255 | */ |
||
256 | 22 | protected function removeReferences($glob) |
|
257 | { |
||
258 | 22 | $checkResults = $this->getReferencesForGlob($glob); |
|
259 | 22 | $nonDeletablePaths = array(); |
|
260 | |||
261 | 22 | foreach ($checkResults as $path => $filesystemPath) { |
|
262 | 22 | if (!array_key_exists($path, $this->json)) { |
|
263 | 22 | $nonDeletablePaths[] = $filesystemPath; |
|
264 | } |
||
265 | } |
||
266 | |||
267 | 22 | if (count($nonDeletablePaths) > 0) { |
|
268 | 2 | throw new InvalidArgumentException(sprintf( |
|
269 | 'You cannot remove resources that are not mapped in the JSON '. |
||
270 | 2 | 'file. Tried to remove %s%s.', |
|
271 | reset($nonDeletablePaths), |
||
272 | 2 | count($nonDeletablePaths) > 1 |
|
273 | ? ' and '.(count($nonDeletablePaths) - 1).' more' |
||
274 | 2 | : '' |
|
275 | )); |
||
276 | } |
||
277 | |||
278 | 20 | $deletedPaths = $this->getReferencesForGlob($glob.'{,/**/*}', self::NO_SEARCH_FILESYSTEM); |
|
279 | 20 | $removed = 0; |
|
280 | |||
281 | 20 | foreach ($deletedPaths as $path => $filesystemPath) { |
|
282 | 20 | $removed += 1 + count($this->getReferencesForGlob($path.'/**/*')); |
|
283 | |||
284 | 20 | unset($this->json[$path]); |
|
285 | } |
||
286 | |||
287 | 20 | return $removed; |
|
288 | } |
||
289 | |||
290 | /** |
||
291 | * {@inheritdoc} |
||
292 | */ |
||
293 | 110 | protected function getReferencesForPath($path) |
|
298 | |||
299 | /** |
||
300 | * {@inheritdoc} |
||
301 | */ |
||
302 | 50 | View Code Duplication | protected function getReferencesForGlob($glob, $flags = 0) |
314 | |||
315 | /** |
||
316 | * {@inheritdoc} |
||
317 | */ |
||
318 | 68 | protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0, $maxDepth = 0) |
|
329 | |||
330 | /** |
||
331 | * {@inheritdoc} |
||
332 | */ |
||
333 | 26 | View Code Duplication | protected function getReferencesInDirectory($path, $flags = 0) |
345 | |||
346 | /** |
||
347 | * Flattens a two-level reference array into a one-level array. |
||
348 | * |
||
349 | * For each entry on the first level, only the first entry of the second |
||
350 | * level is included in the result. |
||
351 | * |
||
352 | * Each reference returned by this method can be: |
||
353 | * |
||
354 | * * `null` |
||
355 | * * a link starting with `@` |
||
356 | * * an absolute filesystem path |
||
357 | * |
||
358 | * The keys of the returned array are Puli paths. Their order is undefined. |
||
359 | * |
||
360 | * @param array $references A two-level reference array as returned by |
||
361 | * {@link searchReferences()}. |
||
362 | * |
||
363 | * @return string[]|null[] A one-level array of references with Puli paths |
||
364 | * as keys. |
||
365 | */ |
||
366 | 110 | private function flatten(array $references) |
|
378 | |||
379 | /** |
||
380 | * Flattens a two-level reference array into a one-level array and filters |
||
381 | * out any references that don't match the given regular expression. |
||
382 | * |
||
383 | * This method takes a two-level reference array as returned by |
||
384 | * {@link searchReferences()}. The references are scanned for Puli paths |
||
385 | * matching the given regular expression. Those matches are returned. |
||
386 | * |
||
387 | * If a matching path refers to more than one reference, the first reference |
||
388 | * is returned in the resulting array. |
||
389 | * |
||
390 | * If `$listDirectories` is set to `true`, all references that contain |
||
391 | * directory paths are traversed recursively and scanned for more paths |
||
392 | * matching the regular expression. This recursive traversal can be limited |
||
393 | * by passing a `$maxDepth` (see {@link getPathDepth()}). |
||
394 | * |
||
395 | * Each reference returned by this method can be: |
||
396 | * |
||
397 | * * `null` |
||
398 | * * a link starting with `@` |
||
399 | * * an absolute filesystem path |
||
400 | * |
||
401 | * The keys of the returned array are Puli paths. Their order is undefined. |
||
402 | * |
||
403 | * @param array $references A two-level reference array as returned by |
||
404 | * {@link searchReferences()}. |
||
405 | * @param string $regex A regular expression used to filter Puli paths. |
||
406 | * @param int $flags A bitwise combination of the flag constants in |
||
407 | * this class. |
||
408 | * @param int $maxDepth The maximum path depth when searching the |
||
409 | * contents of directory references. If 0, the |
||
410 | * depth is unlimited. |
||
411 | * |
||
412 | * @return string[]|null[] A one-level array of references with Puli paths |
||
413 | * as keys. |
||
414 | */ |
||
415 | 68 | private function flattenWithFilter(array $references, $regex, $flags = 0, $maxDepth = 0) |
|
483 | |||
484 | /** |
||
485 | * Filters the JSON file for all references relevant to a given search path. |
||
486 | * |
||
487 | * The JSON is scanned starting with the longest mapped Puli path. |
||
488 | * |
||
489 | * If the search path is "/a/b", the result includes: |
||
490 | * |
||
491 | * * The references of the mapped path "/a/b". |
||
492 | * * The references of any mapped super path "/a" with the sub-path "/b" |
||
493 | * appended. |
||
494 | * |
||
495 | * If the argument `$includeNested` is set to `true`, the result |
||
496 | * additionally includes: |
||
497 | * |
||
498 | * * The references of any mapped sub path "/a/b/c". |
||
499 | * |
||
500 | * This is useful if you want to look for the children of "/a/b" or scan |
||
501 | * all descendants for paths matching a given pattern. |
||
502 | * |
||
503 | * The result of this method is an array with two levels: |
||
504 | * |
||
505 | * * The first level has Puli paths as keys. |
||
506 | * * The second level contains all references for that path, where the |
||
507 | * first reference has the highest, the last reference the lowest |
||
508 | * priority. The keys of the second level are integers. There may be |
||
509 | * holes between any two keys. |
||
510 | * |
||
511 | * The references of the second level contain: |
||
512 | * |
||
513 | * * `null` values for virtual resources |
||
514 | * * strings starting with "@" for links |
||
515 | * * absolute filesystem paths for filesystem resources |
||
516 | * |
||
517 | * @param string $searchPath The path to search. |
||
518 | * @param int $flags A bitwise combination of the flag constants in |
||
519 | * this class. |
||
520 | * |
||
521 | * @return array An array with two levels. |
||
522 | */ |
||
523 | 174 | private function searchReferences($searchPath, $flags = 0) |
|
712 | |||
713 | /** |
||
714 | * Follows any link in a list of references. |
||
715 | * |
||
716 | * This method takes all the given references, checks for links starting |
||
717 | * with "@" and recursively expands those links to their target references. |
||
718 | * The target references may be `null` or absolute filesystem paths. |
||
719 | * |
||
720 | * Null values are returned unchanged. |
||
721 | * |
||
722 | * Absolute filesystem paths are returned unchanged. |
||
723 | * |
||
724 | * @param string[]|null[] $references The references. |
||
725 | * @param int $flags A bitwise combination of the flag |
||
726 | * constants in this class. |
||
727 | * |
||
728 | * @return string[]|null[] The references with all links replaced by their |
||
729 | * target references. If any link pointed to more |
||
730 | * than one target reference, the returned array |
||
731 | * is larger than the passed array (unless the |
||
732 | * argument `$stopOnFirst` was set to `true`). |
||
733 | */ |
||
734 | 118 | private function followLinks(array $references, $flags = 0) |
|
771 | |||
772 | /** |
||
773 | * Appends nested paths to references and filters out the existing ones. |
||
774 | * |
||
775 | * This method takes all the given references, appends the nested path to |
||
776 | * each of them and then filters out the results that actually exist on the |
||
777 | * filesystem. |
||
778 | * |
||
779 | * Null references are filtered out. |
||
780 | * |
||
781 | * Link references should be followed with {@link followLinks()} before |
||
782 | * calling this method. |
||
783 | * |
||
784 | * @param string[]|null[] $references The references. |
||
785 | * @param string $nestedPath The nested path to append without |
||
786 | * leading slash ("/"). |
||
787 | * @param int $flags A bitwise combination of the flag |
||
788 | * constants in this class. |
||
789 | * |
||
790 | * @return string[] The references with the nested path appended. Each |
||
791 | * reference is guaranteed to exist on the filesystem. |
||
792 | */ |
||
793 | 108 | private function appendPathAndFilterExisting(array $references, $nestedPath, $flags = 0) |
|
817 | |||
818 | /** |
||
819 | * Resolves a list of references stored in the JSON. |
||
820 | * |
||
821 | * Each reference passed in can be: |
||
822 | * |
||
823 | * * `null` |
||
824 | * * a link starting with `@` |
||
825 | * * a filesystem path relative to the base directory |
||
826 | * * an absolute filesystem path |
||
827 | * |
||
828 | * Each reference returned by this method can be: |
||
829 | * |
||
830 | * * `null` |
||
831 | * * a link starting with `@` |
||
832 | * * an absolute filesystem path |
||
833 | * |
||
834 | * Additionally, the results are guaranteed to be an array. If the |
||
835 | * argument `$stopOnFirst` is set, that array has a maximum size of 1. |
||
836 | * |
||
837 | * @param string $path The mapped Puli path. |
||
838 | * @param mixed $references The reference(s). |
||
839 | * @param int $flags A bitwise combination of the flag constants in |
||
840 | * this class. |
||
841 | * |
||
842 | * @return string[]|null[] The resolved references. |
||
843 | */ |
||
844 | 174 | private function resolveReferences($path, $references, $flags = 0) |
|
882 | |||
883 | /** |
||
884 | * Returns the depth of a Puli path. |
||
885 | * |
||
886 | * The depth is used in order to limit the recursion when recursively |
||
887 | * iterating directories. |
||
888 | * |
||
889 | * The depth starts at 0 for the root: |
||
890 | * |
||
891 | * / 0 |
||
892 | * /webmozart 1 |
||
893 | * /webmozart/puli 2 |
||
894 | * ... |
||
895 | * |
||
896 | * @param string $path A Puli path. |
||
897 | * |
||
898 | * @return int The depth starting with 0 for the root node. |
||
899 | */ |
||
900 | 26 | private function getPathDepth($path) |
|
908 | |||
909 | /** |
||
910 | * Inserts a path at the beginning of the order list of a mapped path. |
||
911 | * |
||
912 | * @param string $path The path of the mapping where to prepend. |
||
913 | * @param string $prependedPath The path of the mapping to prepend. |
||
914 | */ |
||
915 | 10 | private function prependOrderEntry($path, $prependedPath) |
|
930 | |||
931 | /** |
||
932 | * Initializes a path with the order of the closest parent path. |
||
933 | * |
||
934 | * @param string $path The path to initialize. |
||
935 | * @param array $parentReferences The defined references for parent paths, |
||
936 | * with long paths /a/b sorted before short |
||
937 | * paths /a. |
||
938 | */ |
||
939 | 14 | private function initWithParentOrder($path, array $parentReferences) |
|
951 | |||
952 | /** |
||
953 | * Initializes the order of a path with the default order. |
||
954 | * |
||
955 | * This is necessary if we want to insert a non-default order entry for |
||
956 | * the first time. |
||
957 | * |
||
958 | * @param string $path The path to initialize. |
||
959 | * @param string $insertedPath The path that is being inserted. |
||
960 | * @param array $references The references for each defined path mapping |
||
961 | * in the path chain. |
||
962 | */ |
||
963 | 10 | private function initWithDefaultOrder($path, $insertedPath, $references) |
|
996 | } |
||
997 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.