Total Complexity | 62 |
Total Lines | 554 |
Duplicated Lines | 18.23 % |
Changes | 0 |
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 MetadataLoader 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.
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 MetadataLoader, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | final class MetadataLoader implements LoggerAwareInterface |
||
29 | { |
||
30 | use LoggerAwareTrait; |
||
31 | |||
32 | /** |
||
33 | * The PSR-6 caching service. |
||
34 | * |
||
35 | * @var CacheItemPoolInterface $cachePool |
||
36 | */ |
||
37 | private $cachePool; |
||
38 | |||
39 | /** |
||
40 | * The cache of class/interface lineages. |
||
41 | * |
||
42 | * @var array |
||
43 | */ |
||
44 | private static $lineageCache = []; |
||
45 | |||
46 | /** |
||
47 | * The cache of snake-cased words. |
||
48 | * |
||
49 | * @var array |
||
50 | */ |
||
51 | private static $snakeCache = []; |
||
52 | |||
53 | /** |
||
54 | * The cache of camel-cased words. |
||
55 | * |
||
56 | * @var array |
||
57 | */ |
||
58 | private static $camelCache = []; |
||
59 | |||
60 | /** |
||
61 | * The base path to prepend to any relative paths to search in. |
||
62 | * |
||
63 | * @var string |
||
64 | */ |
||
65 | private $basePath = ''; |
||
66 | |||
67 | /** |
||
68 | * The paths to search in. |
||
69 | * |
||
70 | * @var array |
||
71 | */ |
||
72 | private $paths = []; |
||
73 | |||
74 | /** |
||
75 | * Return new MetadataLoader object. |
||
76 | * |
||
77 | * The application's metadata paths, if any, are merged with |
||
78 | * the loader's search paths. |
||
79 | * |
||
80 | * # Required dependencie |
||
81 | * - `logger` |
||
82 | * - `cache` |
||
83 | * - `paths` |
||
84 | * - `base_path` |
||
85 | * |
||
86 | * @param array $data The loader's dependencies. |
||
87 | * @return void |
||
88 | */ |
||
89 | public function __construct(array $data = null) |
||
90 | { |
||
91 | $this->setLogger($data['logger']); |
||
92 | $this->setCachePool($data['cache']); |
||
93 | $this->setBasePath($data['base_path']); |
||
94 | $this->setPaths($data['paths']); |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * Load the metadata for the given identifier or interfaces. |
||
99 | * |
||
100 | * @param string $ident The metadata identifier to load or |
||
101 | * to use as the cache key if $interfaces is provided. |
||
102 | * @param MetadataInterface $metadata The metadata object to load into. |
||
103 | * @param array|null $interfaces One or more metadata identifiers to load. |
||
104 | * @throws InvalidArgumentException If the identifier is not a string. |
||
105 | * @return MetadataInterface Returns the cached metadata instance or if it's stale or empty, |
||
106 | * loads a fresh copy of the data into $metadata and returns it; |
||
107 | */ |
||
108 | public function load($ident, MetadataInterface $metadata, array $interfaces = null) |
||
109 | { |
||
110 | if (!is_string($ident)) { |
||
111 | throw new InvalidArgumentException( |
||
112 | 'Metadata identifier must be a string' |
||
113 | ); |
||
114 | } |
||
115 | |||
116 | if (is_array($interfaces) && empty($interfaces)) { |
||
117 | $interfaces = null; |
||
118 | } |
||
119 | |||
120 | $cacheKey = 'metadata/'.str_replace('/', '.', $ident); |
||
121 | $cacheItem = $this->cachePool()->getItem($cacheKey); |
||
122 | |||
123 | if (!$cacheItem->isHit()) { |
||
124 | if ($interfaces === null) { |
||
125 | $data = $this->loadData($ident); |
||
126 | } else { |
||
127 | $data = $this->loadDataArray($interfaces); |
||
128 | } |
||
129 | $metadata->setData($data); |
||
130 | |||
131 | $this->cachePool()->save($cacheItem->set($metadata)); |
||
132 | |||
133 | return $metadata; |
||
134 | } |
||
135 | |||
136 | return $cacheItem->get(); |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Fetch the metadata for the given identifier. |
||
141 | * |
||
142 | * @param string $ident The metadata identifier to load. |
||
143 | * @throws InvalidArgumentException If the identifier is not a string. |
||
144 | * @return array |
||
145 | */ |
||
146 | public function loadData($ident) |
||
147 | { |
||
148 | if (!is_string($ident)) { |
||
149 | throw new InvalidArgumentException( |
||
150 | 'Metadata identifier must be a string' |
||
151 | ); |
||
152 | } |
||
153 | |||
154 | $lineage = $this->hierarchy($ident); |
||
155 | $catalog = []; |
||
156 | foreach ($lineage as $id) { |
||
157 | $data = $this->loadFileFromIdent($id); |
||
158 | |||
159 | if (is_array($data)) { |
||
160 | $catalog = array_replace_recursive($catalog, $data); |
||
161 | } |
||
162 | } |
||
163 | |||
164 | return $catalog; |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * Fetch the metadata for the given identifiers. |
||
169 | * |
||
170 | * @param array $idents One or more metadata identifiers to load. |
||
171 | * @return array |
||
172 | */ |
||
173 | public function loadDataArray(array $idents) |
||
174 | { |
||
175 | $catalog = []; |
||
176 | foreach ($idents as $id) { |
||
177 | $data = $this->loadData($id); |
||
178 | |||
179 | if (is_array($data)) { |
||
180 | $catalog = array_replace_recursive($catalog, $data); |
||
181 | } |
||
182 | } |
||
183 | |||
184 | return $catalog; |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Set the cache service. |
||
189 | * |
||
190 | * @param CacheItemPoolInterface $cache A PSR-6 compliant cache pool instance. |
||
191 | * @return self |
||
192 | */ |
||
193 | private function setCachePool(CacheItemPoolInterface $cache) |
||
194 | { |
||
195 | $this->cachePool = $cache; |
||
196 | |||
197 | return $this; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * Retrieve the cache service. |
||
202 | * |
||
203 | * @throws RuntimeException If the cache service was not previously set. |
||
204 | * @return CacheItemPoolInterface |
||
205 | */ |
||
206 | private function cachePool() |
||
207 | { |
||
208 | if (!isset($this->cachePool)) { |
||
209 | throw new RuntimeException( |
||
210 | sprintf('Cache Pool is not defined for "%s"', get_class($this)) |
||
211 | ); |
||
212 | } |
||
213 | |||
214 | return $this->cachePool; |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * Assign a base path for relative search paths. |
||
219 | * |
||
220 | * @param string $basePath The base path to use. |
||
221 | * @throws InvalidArgumentException If the base path parameter is not a string. |
||
222 | * @return self |
||
223 | */ |
||
224 | View Code Duplication | private function setBasePath($basePath) |
|
|
|||
225 | { |
||
226 | if (!is_string($basePath)) { |
||
227 | throw new InvalidArgumentException( |
||
228 | 'Base path must be a string' |
||
229 | ); |
||
230 | } |
||
231 | |||
232 | $basePath = realpath($basePath); |
||
233 | $this->basePath = rtrim($basePath, '/\\').DIRECTORY_SEPARATOR; |
||
234 | |||
235 | return $this; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * Retrieve the base path for relative search paths. |
||
240 | * |
||
241 | * @return string |
||
242 | */ |
||
243 | private function basePath() |
||
244 | { |
||
245 | return $this->basePath; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * Assign a list of paths. |
||
250 | * |
||
251 | * @param string[] $paths The list of paths to add. |
||
252 | * @return self |
||
253 | */ |
||
254 | private function setPaths(array $paths) |
||
255 | { |
||
256 | $this->paths = []; |
||
257 | $this->addPaths($paths); |
||
258 | |||
259 | return $this; |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * Retrieve the searchable paths. |
||
264 | * |
||
265 | * @return string[] |
||
266 | */ |
||
267 | private function paths() |
||
268 | { |
||
269 | return $this->paths; |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * Append a list of paths. |
||
274 | * |
||
275 | * @param string[] $paths The list of paths to add. |
||
276 | * @return self |
||
277 | */ |
||
278 | private function addPaths(array $paths) |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Append a path. |
||
289 | * |
||
290 | * @param string $path A file or directory path. |
||
291 | * @throws InvalidArgumentException If the path does not exist or is invalid. |
||
292 | * @return self |
||
293 | */ |
||
294 | View Code Duplication | private function addPath($path) |
|
295 | { |
||
296 | $path = $this->resolvePath($path); |
||
297 | |||
298 | if ($this->validatePath($path)) { |
||
299 | $this->paths[] = $path; |
||
300 | } |
||
301 | |||
302 | return $this; |
||
303 | } |
||
304 | |||
305 | /** |
||
306 | * Parse a relative path using the base path if needed. |
||
307 | * |
||
308 | * @param string $path The path to resolve. |
||
309 | * @throws InvalidArgumentException If the path is invalid. |
||
310 | * @return string |
||
311 | */ |
||
312 | View Code Duplication | private function resolvePath($path) |
|
313 | { |
||
314 | if (!is_string($path)) { |
||
315 | throw new InvalidArgumentException( |
||
316 | 'Path needs to be a string' |
||
317 | ); |
||
318 | } |
||
319 | |||
320 | $basePath = $this->basePath(); |
||
321 | $path = ltrim($path, '/\\'); |
||
322 | |||
323 | if ($basePath && strpos($path, $basePath) === false) { |
||
324 | $path = $basePath.$path; |
||
325 | } |
||
326 | |||
327 | return $path; |
||
328 | } |
||
329 | |||
330 | /** |
||
331 | * Validate a resolved path. |
||
332 | * |
||
333 | * @param string $path The path to validate. |
||
334 | * @return string |
||
335 | */ |
||
336 | private function validatePath($path) |
||
337 | { |
||
338 | return file_exists($path); |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Build a class/interface lineage from the given snake-cased namespace. |
||
343 | * |
||
344 | * @param string $ident The FQCN (in snake-case) to load the hierarchy from. |
||
345 | * @return array |
||
346 | */ |
||
347 | private function hierarchy($ident) |
||
348 | { |
||
349 | if (!is_string($ident)) { |
||
350 | return []; |
||
351 | } |
||
352 | |||
353 | if (isset(static::$lineageCache[$ident])) { |
||
354 | return static::$lineageCache[$ident]; |
||
355 | } |
||
356 | |||
357 | $classname = $this->identToClassname($ident); |
||
358 | |||
359 | return $this->classLineage($classname, $ident); |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * Build a class/interface lineage from the given PHP namespace. |
||
364 | * |
||
365 | * @param string $classname The FQCN to load the hierarchy from. |
||
366 | * @param string|null $ident Optional. The snake-cased $classname. |
||
367 | * @return array |
||
368 | */ |
||
369 | private function classLineage($classname, $ident = null) |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * Load a metadata file from the given metdata identifier. |
||
412 | * |
||
413 | * The file is converted to JSON, the only supported format. |
||
414 | * |
||
415 | * @param string $ident The metadata identifier to fetch. |
||
416 | * @return array|null |
||
417 | */ |
||
418 | private function loadFileFromIdent($ident) |
||
419 | { |
||
420 | $filename = $this->filenameFromIdent($ident); |
||
421 | |||
422 | return $this->loadFile($filename); |
||
423 | } |
||
424 | |||
425 | /** |
||
426 | * Load a metadata file. |
||
427 | * |
||
428 | * Supported file types: JSON. |
||
429 | * |
||
430 | * @param string $filename A supported metadata file. |
||
431 | * @return array|null |
||
432 | */ |
||
433 | View Code Duplication | private function loadFile($filename) |
|
434 | { |
||
435 | if (file_exists($filename)) { |
||
436 | return $this->loadJsonFile($filename); |
||
437 | } |
||
438 | |||
439 | $paths = $this->paths(); |
||
440 | |||
441 | if (empty($paths)) { |
||
442 | return null; |
||
443 | } |
||
444 | |||
445 | foreach ($paths as $basePath) { |
||
446 | $file = $basePath.DIRECTORY_SEPARATOR.$filename; |
||
447 | if (file_exists($file)) { |
||
448 | return $this->loadJsonFile($file); |
||
449 | } |
||
450 | } |
||
451 | |||
452 | return null; |
||
453 | } |
||
454 | |||
455 | /** |
||
456 | * Load the contents of a JSON file. |
||
457 | * |
||
458 | * @param mixed $filename The file path to retrieve. |
||
459 | * @throws InvalidArgumentException If a JSON decoding error occurs. |
||
460 | * @return array|null |
||
461 | */ |
||
462 | View Code Duplication | private function loadJsonFile($filename) |
|
500 | ); |
||
501 | } |
||
502 | |||
503 | /** |
||
504 | * Convert a snake-cased namespace to a file path. |
||
505 | * |
||
506 | * @param string $ident The identifier to convert. |
||
507 | * @return string |
||
508 | */ |
||
509 | private function filenameFromIdent($ident) |
||
510 | { |
||
511 | $filename = str_replace([ '\\' ], '.', $ident); |
||
512 | $filename .= '.json'; |
||
513 | |||
514 | return $filename; |
||
515 | } |
||
516 | |||
517 | /** |
||
518 | * Convert a snake-cased namespace to CamelCase. |
||
519 | * |
||
520 | * @param string $ident The namespace to convert. |
||
521 | * @return string Returns a valid PHP namespace. |
||
522 | */ |
||
523 | private function identToClassname($ident) |
||
558 | } |
||
559 | |||
560 | /** |
||
561 | * Convert a CamelCase namespace to snake-case. |
||
562 | * |
||
563 | * @param string $classname The namespace to convert. |
||
564 | * @return string Returns a snake-cased namespace. |
||
565 | */ |
||
566 | private function classnameToIdent($classname) |
||
582 | } |
||
583 | } |
||
584 |
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.