Total Complexity | 47 |
Total Lines | 577 |
Duplicated Lines | 4.16 % |
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 ImageStorage 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 ImageStorage, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | class ImageStorage implements Storage |
||
29 | { |
||
30 | |||
31 | /** |
||
32 | * The directory to store the images in |
||
33 | * |
||
34 | * @var Directory |
||
35 | */ |
||
36 | private $directory; |
||
37 | |||
38 | /** |
||
39 | * The image type -> file extension map |
||
40 | * |
||
41 | * @var array |
||
42 | */ |
||
43 | private $extensions = array(Image::JPEG => 'jpg', Image::PNG => 'png', Image::GIF => 'gif'); |
||
44 | |||
45 | /** |
||
46 | * The image type -> MIME type map |
||
47 | * |
||
48 | * @var array |
||
49 | */ |
||
50 | private $mimeTypes = array(Image::JPEG => 'image/jpeg', Image::PNG => 'image/png', Image::GIF => 'image/gif'); |
||
51 | |||
52 | /** |
||
53 | * The public accessible URL of the cache directory |
||
54 | * |
||
55 | * @var string |
||
56 | */ |
||
57 | private $baseUrl; |
||
58 | |||
59 | /** |
||
60 | * The cache directory |
||
61 | * |
||
62 | * @var Directory |
||
63 | */ |
||
64 | private $cacheDirectory; |
||
65 | |||
66 | |||
67 | /** |
||
68 | * Constructs the image file storage from the given arguments. |
||
69 | * |
||
70 | * @param string $directory The directory to store the images in |
||
71 | * @param string $cacheDirectory |
||
72 | * @param string $baseUrl |
||
73 | * @param boolean $tryCreateDirectories |
||
74 | */ |
||
75 | public function __construct($directory, $cacheDirectory, $baseUrl = '/cache/', $tryCreateDirectories = true) |
||
76 | { |
||
77 | $this->directory = new Directory($directory, $tryCreateDirectories); |
||
78 | $this->cacheDirectory = new Directory($cacheDirectory, $tryCreateDirectories); |
||
79 | |||
80 | if ($this->directory->is($this->cacheDirectory)) { |
||
81 | throw new InvalidCacheDirectoryException($cacheDirectory); |
||
82 | } |
||
83 | |||
84 | $this->setBaseUrl($baseUrl); |
||
85 | } |
||
86 | |||
87 | |||
88 | /** |
||
89 | * Fetches the cached copy of an image by given request. |
||
90 | * |
||
91 | * @param Request $request The image request |
||
92 | * @return Image |
||
93 | */ |
||
94 | public function fetch(Request $request) |
||
95 | { |
||
96 | $this->checkImage($request); |
||
97 | $filename = $this->createCacheFilename($request); |
||
98 | |||
99 | return Image::fromFile($filename); |
||
100 | } |
||
101 | |||
102 | |||
103 | /** |
||
104 | * Fetches the original image by the given image meta information. |
||
105 | * |
||
106 | * @param Meta $meta The stored image meta information |
||
107 | * @return Image The stored image |
||
108 | */ |
||
109 | public function original(Meta $meta) |
||
110 | { |
||
111 | return Image::fromFile($this->createFilename($meta)); |
||
112 | } |
||
113 | |||
114 | |||
115 | /** |
||
116 | * Removes the image from the storage by the given image meta information. |
||
117 | * |
||
118 | * @param Meta $meta The image information |
||
119 | * @return ImageStorage Fluent interface |
||
120 | */ |
||
121 | public function remove(Meta $meta) |
||
122 | { |
||
123 | unlink($this->createFilename($meta)); |
||
124 | |||
125 | return $this; |
||
126 | } |
||
127 | |||
128 | |||
129 | /** |
||
130 | * Checks if an image of the given meta information is stored in the storage. |
||
131 | * |
||
132 | * @param Meta $meta The image meta information |
||
133 | * @return boolean TRUE if image is present in the storage or FALSE otherwise |
||
134 | */ |
||
135 | public function contains(Meta $meta) |
||
136 | { |
||
137 | return file_exists($this->createFilename($meta)); |
||
138 | } |
||
139 | |||
140 | |||
141 | /** |
||
142 | * Stores the given uploaded file. |
||
143 | * |
||
144 | * @param FileUpload $upload |
||
145 | * @param meta $meta |
||
146 | * @return ImageStorage Fluent interface |
||
147 | * @throws UploaderException |
||
148 | */ |
||
149 | public function upload(FileUpload $upload, Meta $meta) |
||
150 | { |
||
151 | if ($upload->getError()) { |
||
152 | throw new UploaderException($upload->getError()); |
||
153 | } |
||
154 | |||
155 | $source = $upload->getTemporaryFile(); |
||
156 | $this->readMeta($source, $meta); |
||
157 | $target = $this->createFilename($meta); |
||
158 | |||
159 | if (!$this->contains($meta)) { |
||
160 | $image = Image::fromFile($source); |
||
161 | $image->resize(1920, 1080); |
||
162 | $image->save($target); |
||
163 | } |
||
164 | if (file_exists($source)) { |
||
165 | |||
166 | unlink($source); |
||
167 | } |
||
168 | |||
169 | return $this; |
||
170 | } |
||
171 | |||
172 | |||
173 | /** |
||
174 | * Returns the URL of the cached version of the image. |
||
175 | * |
||
176 | * @param Request $request The image request |
||
177 | * @return string The URL of the image |
||
178 | */ |
||
179 | public function link(Request $request) |
||
180 | { |
||
181 | $this->checkImage($request); |
||
182 | $filePath = $this->createFilePath($request); |
||
183 | |||
184 | return "$this->baseUrl$filePath"; |
||
185 | } |
||
186 | |||
187 | |||
188 | /** |
||
189 | * Creates the file download HTTP response which can be easily sent using the `send()` method. |
||
190 | * |
||
191 | * @param Request $request The image request |
||
192 | * @return FileResponse |
||
193 | */ |
||
194 | public function download(Request $request) |
||
195 | { |
||
196 | $this->checkImage($request); |
||
197 | $filename = $this->createCacheFilename($request); |
||
198 | |||
199 | return new FileResponse($filename, basename($filename), $request->getMeta()->getType()); |
||
200 | } |
||
201 | |||
202 | |||
203 | /** |
||
204 | * Renders the image directly to the standard output. |
||
205 | * |
||
206 | * @param Request $request The image request |
||
207 | * @return ImageStorage Fluent interface |
||
208 | */ |
||
209 | public function send(Request $request) |
||
210 | { |
||
211 | $this->checkImage($request); |
||
212 | $filename = $this->createCacheFilename($request); |
||
213 | $fp = fopen($filename, 'r'); |
||
214 | header("Content-type: " . $this->getMimeType($request->getMeta()->getType())); |
||
215 | fpassthru($fp); |
||
216 | fclose($fp); |
||
217 | |||
218 | return $this; |
||
219 | } |
||
220 | |||
221 | |||
222 | /** |
||
223 | * Checks that the cached version of the image exists and creates it if not. |
||
224 | * |
||
225 | * @param Request $request The image request |
||
226 | * @return string The file name of the existing cached version of an image |
||
227 | */ |
||
228 | private function checkImage(Request $request) |
||
229 | { |
||
230 | $filename = $this->createCacheFilename($request); |
||
231 | |||
232 | if (!file_exists($filename)) { |
||
233 | |||
234 | $meta = $request->getMeta(); |
||
235 | |||
236 | if (!$this->contains($meta)) { |
||
237 | |||
238 | // Use another meta |
||
239 | $meta = new EmptyImage(); |
||
240 | $request->setMeta($meta); |
||
241 | $filename = $this->createCacheFilename($request); |
||
242 | |||
243 | } |
||
244 | |||
245 | $original = $this->original($meta); |
||
246 | |||
247 | if ($request->getCrop()) { |
||
248 | $image = $this->crop($original, $request); |
||
249 | } else { |
||
250 | $image = $this->resize($original, $request); |
||
251 | } |
||
252 | new Directory(dirname($filename)); |
||
253 | $image->save($filename, 75, $meta->getType()); |
||
254 | } |
||
255 | } |
||
256 | |||
257 | |||
258 | /** |
||
259 | * Returns the file name of the cached version of the image. |
||
260 | * |
||
261 | * @param Request $request |
||
262 | * @return string The file name of the cached version of the image |
||
263 | */ |
||
264 | private function createCacheFilename(Request $request) |
||
265 | { |
||
266 | $filePath = $this->createFilePath($request); |
||
267 | |||
268 | return "$this->cacheDirectory/$filePath"; |
||
269 | } |
||
270 | |||
271 | |||
272 | /** |
||
273 | * Crops the given image using the given image request options. |
||
274 | * |
||
275 | * @param Image $image The image to resize |
||
276 | * @param Request $request The image request |
||
277 | * @return Image The image thumbnail |
||
278 | */ |
||
279 | private function crop(Image $image, Request $request) |
||
280 | { |
||
281 | if ($request->getDimensions() === Request::ORIGINAL) { |
||
282 | |||
283 | return $image; |
||
284 | } |
||
285 | |||
286 | list($width, $height) = $this->processDimensions($request->getDimensions()); |
||
287 | |||
288 | $resizeWidth = $width; |
||
289 | $resizeHeight = $height; |
||
290 | |||
291 | $originalWidth = $request->getMeta()->getWidth(); |
||
292 | $originalHeight = $request->getMeta()->getHeight(); |
||
293 | $originalLandscape = $originalWidth > $originalHeight; |
||
294 | |||
295 | $cropLandscape = $width > $height; |
||
296 | $equals = $width === $height; |
||
297 | |||
298 | if ($originalLandscape) { |
||
299 | |||
300 | if ($cropLandscape) { |
||
301 | |||
302 | $coefficient = $originalHeight / $height; |
||
303 | $scaledWidth = round($originalWidth / $coefficient); |
||
304 | |||
305 | $left = round(($scaledWidth - $width) / 2); |
||
306 | $top = 0; |
||
307 | |||
308 | View Code Duplication | if ($scaledWidth < $width) { |
|
309 | $coefficient = $originalWidth / $width; |
||
310 | $scaledHeight = round($originalHeight / $coefficient); |
||
311 | |||
312 | $left = 0; |
||
313 | $top = round(($scaledHeight - $height) / 2); |
||
314 | } |
||
315 | |||
316 | View Code Duplication | } else { |
|
317 | |||
318 | $coefficient = $originalHeight / $height; |
||
319 | $scaledWidth = round($originalWidth / $coefficient); |
||
320 | |||
321 | $left = round(($scaledWidth - $width) / 2); |
||
322 | $top = 0; |
||
323 | } |
||
324 | |||
325 | } else { |
||
326 | |||
327 | if ($cropLandscape || $equals) { |
||
328 | |||
329 | $coefficient = $originalWidth / $width; |
||
330 | $scaledHeight = round($originalHeight / $coefficient); |
||
331 | |||
332 | $left = 0; |
||
333 | $top = round(($scaledHeight - $height) / 2); |
||
334 | |||
335 | View Code Duplication | } else { |
|
336 | |||
337 | $coefficient = $originalHeight / $height; |
||
338 | $scaledWidth = round($originalWidth / $coefficient); |
||
339 | |||
340 | $left = round(($scaledWidth - $width) / 2); |
||
341 | $top = 0; |
||
342 | |||
343 | } |
||
344 | } |
||
345 | |||
346 | $image->resize($resizeWidth, $resizeHeight, Image::FILL); |
||
347 | $image->crop($left, $top, $width, $height); |
||
348 | |||
349 | return $image; |
||
350 | } |
||
351 | |||
352 | |||
353 | /** |
||
354 | * Resizes the given image to the given dimensions using given flags. |
||
355 | * |
||
356 | * @param Image $image The image to resize |
||
357 | * @param Request $request The image request |
||
358 | * @return Image The image thumbnail |
||
359 | */ |
||
360 | private function resize(Image $image, Request $request) |
||
361 | { |
||
362 | if ($request->getDimensions() === Request::ORIGINAL) { |
||
363 | |||
364 | return $image; |
||
365 | } |
||
366 | |||
367 | list($width, $height) = $this->processDimensions($request->getDimensions()); |
||
368 | |||
369 | return $image->resize($width, $height, $request->getFlags()); |
||
370 | } |
||
371 | |||
372 | |||
373 | /** |
||
374 | * Returns the part of the image filename relative to the cache directory. |
||
375 | * |
||
376 | * @param Request $request The image request |
||
377 | * @return string |
||
378 | */ |
||
379 | private function createFilePath(Request $request) |
||
380 | { |
||
381 | $dimensions = $request->getDimensions(); |
||
382 | $flags = $request->getFlags(); |
||
383 | $crop = (int) $request->getCrop(); |
||
384 | |||
385 | $meta = $request->getMeta(); |
||
386 | $ext = $this->getExtension($meta->getType()); |
||
387 | $baseFilePath = $this->createCacheDirectoryPath($meta->getHash()); |
||
388 | |||
389 | return "$baseFilePath.$dimensions.$flags.$crop.$ext"; |
||
390 | } |
||
391 | |||
392 | |||
393 | /** |
||
394 | * Parses the given dimensions string for the image width and height. |
||
395 | * |
||
396 | * @param string $dimensions The dimensions string |
||
397 | * @return array The width and height of the image in pixels |
||
398 | */ |
||
399 | private function processDimensions($dimensions) |
||
400 | { |
||
401 | if (strpos($dimensions, 'x') !== false) { |
||
402 | list($width, $height) = explode('x', $dimensions); // different dimensions, eg. "210x150" |
||
403 | $width = intval($width); |
||
404 | $height = intval($height); |
||
405 | } else { |
||
406 | $width = intval($dimensions); // same dimensions, eg. "210" => 210x210 |
||
407 | $height = $width; |
||
408 | } |
||
409 | |||
410 | return array($width, $height); |
||
411 | } |
||
412 | |||
413 | |||
414 | /** |
||
415 | * Returns the file extension for the given image type. |
||
416 | * |
||
417 | * @param integer $type The image type |
||
418 | * @return string The file extension |
||
419 | * @throws ImageTypeException |
||
420 | */ |
||
421 | private function getExtension($type) |
||
430 | } |
||
431 | |||
432 | |||
433 | /** |
||
434 | * Creates the internal directory path from the given hash. |
||
435 | * |
||
436 | * Some special images like the "Image Not Available" image are stored |
||
437 | * in directories prefixed with an underscore. Those directories are not |
||
438 | * fragmented to hash based structure. |
||
439 | * |
||
440 | * @param string $hash Tha SHA1 hash |
||
441 | * @return string |
||
442 | */ |
||
443 | private function createCacheDirectoryPath($hash) |
||
444 | { |
||
445 | if ($hash{0} === '_') { |
||
446 | return "$hash/$hash"; |
||
447 | } |
||
448 | |||
449 | return "$hash[0]$hash[1]/$hash[2]$hash[3]" . '/' . $hash; |
||
450 | } |
||
451 | |||
452 | |||
453 | /** |
||
454 | * Creates the absolute file name from the given meta information. |
||
455 | * |
||
456 | * @param Meta $meta The image meta information |
||
457 | * @return string The absolute file name |
||
458 | */ |
||
459 | private function createFilename(Meta $meta) |
||
460 | { |
||
461 | $path = $this->createDirectoryPath($meta->getHash()); |
||
462 | $ext = $this->getExtension($meta->getType()); |
||
463 | $hash = $meta->getHash(); |
||
464 | |||
465 | $filename = "$this->directory/$path/$hash.$ext"; |
||
466 | |||
467 | new Directory(dirname($filename)); |
||
468 | |||
469 | return $filename; |
||
470 | } |
||
471 | |||
472 | |||
473 | /** |
||
474 | * Creates the internal directory path from the given hash. |
||
475 | * |
||
476 | * Some special images like the "Image Not Available" image are stored |
||
477 | * in directories prefixed with an underscore. Those directories are not |
||
478 | * fragmented to hash based structure. |
||
479 | * |
||
480 | * @param string $hash Tha SHA1 hash |
||
481 | * @return string |
||
482 | */ |
||
483 | private function createDirectoryPath($hash) |
||
484 | { |
||
485 | if ($hash{0} === '_') { |
||
486 | return $hash; |
||
487 | } |
||
488 | |||
489 | return "$hash[0]$hash[1]/$hash[2]$hash[3]"; |
||
490 | } |
||
491 | |||
492 | |||
493 | /** |
||
494 | * Reads the image type from the given `$filename`, computes the image hash |
||
495 | * and store these information in the given `$meta`. |
||
496 | * |
||
497 | * @param string $filename The file to read the meta-data from |
||
498 | * @param Meta $meta The object to store meta-data in |
||
499 | */ |
||
500 | private function readMeta($filename, Meta $meta) |
||
501 | { |
||
502 | $info = getimagesize($filename); |
||
503 | $meta->setHash($this->hash($filename)); |
||
504 | $meta->setWidth($info[0]); |
||
505 | $meta->setHeight($info[1]); |
||
506 | $meta->setType($info[2]); |
||
507 | } |
||
508 | |||
509 | |||
510 | /** |
||
511 | * Computes the SHA1 hash for the given file. |
||
512 | * |
||
513 | * @param string $filename The file to compute the hash from |
||
514 | * @return string The SHA1 hash of a file |
||
515 | */ |
||
516 | private function hash($filename) |
||
517 | { |
||
518 | return sha1_file($filename); |
||
519 | } |
||
520 | |||
521 | |||
522 | /** |
||
523 | * Returns the image MIME type according to the given image type. |
||
524 | * |
||
525 | * @param integer $imageType Image type |
||
526 | * @return string |
||
527 | */ |
||
528 | protected function getMimeType($imageType) |
||
529 | { |
||
530 | return $this->mimeTypes[$imageType]; |
||
531 | } |
||
532 | |||
533 | |||
534 | /** |
||
535 | * Returns the original stored image filename. |
||
536 | * |
||
537 | * @param Meta $meta Image meta information |
||
538 | * @return string |
||
539 | */ |
||
540 | public function file(Meta $meta) |
||
541 | { |
||
542 | return $this->createFilename($meta); |
||
543 | } |
||
544 | |||
545 | |||
546 | /** |
||
547 | * Returns the public accessible cache directory URL. |
||
548 | * |
||
549 | * @return string |
||
550 | */ |
||
551 | public function getBaseUrl() |
||
552 | { |
||
553 | return $this->baseUrl; |
||
554 | } |
||
555 | |||
556 | |||
557 | /** |
||
558 | * Sets the public accessible cache directory URL. |
||
559 | * |
||
560 | * @param string $baseUrl |
||
561 | * @return $this |
||
562 | */ |
||
563 | protected function setBaseUrl($baseUrl) |
||
564 | { |
||
565 | if (!Strings::endsWith($baseUrl, '/')) { |
||
566 | $baseUrl .= '/'; |
||
567 | } |
||
568 | $this->baseUrl = $baseUrl; |
||
569 | |||
570 | return $this; |
||
571 | } |
||
572 | |||
573 | |||
574 | public function rotate(Meta $meta, $deg = 90) |
||
575 | { |
||
576 | $original = $this->createFilename($meta); |
||
577 | $backupName = $this->cacheDirectory . '/__rotate-backup'; |
||
578 | |||
579 | // Create rotated image |
||
580 | copy($original, $backupName); |
||
581 | |||
582 | exec(sprintf("convert -rotate $deg -strip %s %s", $original, $backupName)); |
||
583 | |||
584 | // Add new rotated image to storage |
||
585 | $this->add(new ImageFile($backupName), $meta); |
||
586 | |||
587 | unlink($backupName); |
||
588 | |||
589 | return $this; |
||
590 | } |
||
591 | |||
592 | |||
593 | /** |
||
594 | * Adds an image from the given file to the storage. |
||
595 | * |
||
596 | * @param ImageFile $file The image file to add |
||
597 | * @param Meta $meta |
||
598 | * @return boolean TRUE on success or FALSE on failure |
||
599 | */ |
||
600 | public function add(ImageFile $file, Meta $meta) |
||
605 | } |
||
606 | |||
607 | |||
608 | } |
||
609 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths