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 Image 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 Image, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 59 | class Image |
||
| 60 | { |
||
| 61 | /** |
||
| 62 | * The title of the image for alt text (optional). |
||
| 63 | * |
||
| 64 | * @var string |
||
| 65 | * |
||
| 66 | * @since 1.0 |
||
| 67 | */ |
||
| 68 | private $title; |
||
|
|
|||
| 69 | |||
| 70 | /** |
||
| 71 | * The absolute path to the source image. |
||
| 72 | * |
||
| 73 | * @var string |
||
| 74 | * |
||
| 75 | * @since 1.0 |
||
| 76 | */ |
||
| 77 | private $source; |
||
| 78 | |||
| 79 | /** |
||
| 80 | * The width of the image (can differ from the source file when scale=true). |
||
| 81 | * |
||
| 82 | * @var Alpha\Model\Type\Integer |
||
| 83 | * |
||
| 84 | * @since 1.0 |
||
| 85 | */ |
||
| 86 | private $width; |
||
| 87 | |||
| 88 | /** |
||
| 89 | * The height of the image (can differ from the source file when scale=true). |
||
| 90 | * |
||
| 91 | * @var Alpha\Model\Type\Integer |
||
| 92 | * |
||
| 93 | * @since 1.0 |
||
| 94 | */ |
||
| 95 | private $height; |
||
| 96 | |||
| 97 | /** |
||
| 98 | * The file type of the source image (gif, jpg, or png supported). |
||
| 99 | * |
||
| 100 | * @var Alpha\Model\Type\Enum |
||
| 101 | * |
||
| 102 | * @since 1.0 |
||
| 103 | */ |
||
| 104 | private $sourceType; |
||
| 105 | |||
| 106 | /** |
||
| 107 | * The quality of the jpg image generated (0.00 to 1.00, 0.75 by default). |
||
| 108 | * |
||
| 109 | * @var Alpha\Model\Type\Double |
||
| 110 | * |
||
| 111 | * @since 1.0 |
||
| 112 | */ |
||
| 113 | private $quality; |
||
| 114 | |||
| 115 | /** |
||
| 116 | * Flag to determine if the image will scale to match the target resolution (false |
||
| 117 | * by default). |
||
| 118 | * |
||
| 119 | * @var Alpha\Model\Type\Boolean |
||
| 120 | * |
||
| 121 | * @since 1.0 |
||
| 122 | */ |
||
| 123 | private $scale; |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Flag to determine if the link to the image will change every 24hrs, making hot-linking |
||
| 127 | * to the image difficult (false by default). |
||
| 128 | * |
||
| 129 | * @var Alpha\Model\Type\Boolean |
||
| 130 | * |
||
| 131 | * @since 1.0 |
||
| 132 | */ |
||
| 133 | private $secure; |
||
| 134 | |||
| 135 | /** |
||
| 136 | * The auto-generated name of the cache file for the image. |
||
| 137 | * |
||
| 138 | * @var string |
||
| 139 | * |
||
| 140 | * @since 1.0 |
||
| 141 | */ |
||
| 142 | private $filename; |
||
| 143 | |||
| 144 | /** |
||
| 145 | * Trace logger. |
||
| 146 | * |
||
| 147 | * @var Alpha\Util\Logging\Logger |
||
| 148 | * |
||
| 149 | * @since 1.0 |
||
| 150 | */ |
||
| 151 | private static $logger = null; |
||
| 152 | |||
| 153 | /** |
||
| 154 | * The constructor. |
||
| 155 | * |
||
| 156 | * @param $source |
||
| 157 | * @param $width |
||
| 158 | * @param $height |
||
| 159 | * @param $sourceType |
||
| 160 | * @param $quality |
||
| 161 | * @param $scale |
||
| 162 | * |
||
| 163 | * @throws Alpha\Exception\IllegalArguementException |
||
| 164 | * |
||
| 165 | * @since 1.0 |
||
| 166 | */ |
||
| 167 | public function __construct($source, $width, $height, $sourceType, $quality = 0.75, $scale = false, $secure = false) |
||
| 168 | { |
||
| 169 | self::$logger = new Logger('Image'); |
||
| 170 | self::$logger->debug('>>__construct(source=['.$source.'], width=['.$width.'], height=['.$height.'], sourceType=['.$sourceType.'], quality=['.$quality.'], scale=['.$scale.'], secure=['.$secure.'])'); |
||
| 171 | |||
| 172 | $config = ConfigProvider::getInstance(); |
||
| 173 | |||
| 174 | if (file_exists($source)) { |
||
| 175 | $this->source = $source; |
||
| 176 | } else { |
||
| 177 | throw new IllegalArguementException('The source file for the Image widget ['.$source.'] cannot be found!'); |
||
| 178 | } |
||
| 179 | |||
| 180 | $this->sourceType = new Enum(); |
||
| 181 | $this->sourceType->setOptions(array('jpg', 'png', 'gif')); |
||
| 182 | $this->sourceType->setValue($sourceType); |
||
| 183 | |||
| 184 | if ($quality < 0.0 || $quality > 1.0) { |
||
| 185 | throw new IllegalArguementException('The quality setting of ['.$quality.'] is outside of the allowable range of 0.0 to 1.0'); |
||
| 186 | } |
||
| 187 | |||
| 188 | $this->quality = new Double($quality); |
||
| 189 | $this->scale = new Boolean($scale); |
||
| 190 | $this->secure = new Boolean($secure); |
||
| 191 | |||
| 192 | $this->width = new Integer($width); |
||
| 193 | $this->height = new Integer($height); |
||
| 194 | |||
| 195 | $this->setFilename(); |
||
| 196 | |||
| 197 | self::$logger->debug('<<__construct'); |
||
| 198 | } |
||
| 199 | |||
| 200 | /** |
||
| 201 | * Renders the HTML <img> tag to the ViewImage controller, with all of the correct params to render the source |
||
| 202 | * image in the desired resolution. |
||
| 203 | * |
||
| 204 | * @param $altText Set this value to render alternate text as part of the HTML link (defaults to no alternate text) |
||
| 205 | * |
||
| 206 | * @return string |
||
| 207 | * |
||
| 208 | * @since 1.0 |
||
| 209 | */ |
||
| 210 | public function renderHTMLLink($altText = '') |
||
| 211 | { |
||
| 212 | $config = ConfigProvider::getInstance(); |
||
| 213 | |||
| 214 | if ($this->secure->getBooleanValue()) { |
||
| 215 | $params = Controller::generateSecurityFields(); |
||
| 216 | |||
| 217 | return '<img src="'.FrontController::generateSecureURL('act=Alpha\Controller\ImageController&source='.$this->source.'&width='.$this->width->getValue().'&height='.$this->height->getValue().'&type='.$this->sourceType->getValue().'&quality='.$this->quality->getValue().'&scale='.$this->scale->getValue().'&secure='.$this->secure->getValue().'&var1='.$params[0].'&var2='.$params[1]).'"'.(empty($altText) ? '' : ' alt="'.$altText.'"').($config->get('cms.images.widget.bootstrap.responsive') ? ' class="img-responsive"' : '').'/>'; |
||
| 218 | } else { |
||
| 219 | return '<img src="'.FrontController::generateSecureURL('act=Alpha\Controller\ImageController&source='.$this->source.'&width='.$this->width->getValue().'&height='.$this->height->getValue().'&type='.$this->sourceType->getValue().'&quality='.$this->quality->getValue().'&scale='.$this->scale->getValue().'&secure='.$this->secure->getValue()).'"'.(empty($altText) ? '' : ' alt="'.$altText.'"').($config->get('cms.images.widget.bootstrap.responsive') ? ' class="img-responsive"' : '').'/>'; |
||
| 220 | } |
||
| 221 | } |
||
| 222 | |||
| 223 | /** |
||
| 224 | * Setter for the filename, which also creates a sub-directory under /cache for images when required. |
||
| 225 | * |
||
| 226 | * @since 1.0 |
||
| 227 | * |
||
| 228 | * @throws Alpha\Exception\AlphaException |
||
| 229 | */ |
||
| 230 | private function setFilename() |
||
| 231 | { |
||
| 232 | $config = ConfigProvider::getInstance(); |
||
| 233 | |||
| 234 | if (!strpos($this->source, 'attachments/article_')) { |
||
| 235 | // checking to see if we will write a jpg or png to the cache |
||
| 236 | View Code Duplication | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
|
| 237 | $this->filename = $config->get('app.file.store.dir').'cache/images/'.basename($this->source, '.'.$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.png'; |
||
| 238 | } else { |
||
| 239 | $this->filename = $config->get('app.file.store.dir').'cache/images/'.basename($this->source, '.'.$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.jpg'; |
||
| 240 | } |
||
| 241 | } else { |
||
| 242 | // make a cache dir for the article |
||
| 243 | $cacheDir = $config->get('app.file.store.dir').'cache/images/article_'.mb_substr($this->source, mb_strpos($this->source, 'attachments/article_') + 20, 11); |
||
| 244 | if (!file_exists($cacheDir)) { |
||
| 245 | $success = mkdir($cacheDir); |
||
| 246 | |||
| 247 | if (!$success) { |
||
| 248 | throw new AlphaException('Unable to create the folder '.$cacheDir.' for the cache image, source file is '.$this->source); |
||
| 249 | } |
||
| 250 | |||
| 251 | // ...and set write permissions on the folder |
||
| 252 | $success = chmod($cacheDir, 0777); |
||
| 253 | |||
| 254 | if (!$success) { |
||
| 255 | throw new AlphaException('Unable to set write permissions on the folder ['.$cacheDir.'].'); |
||
| 256 | } |
||
| 257 | } |
||
| 258 | |||
| 259 | // now set the filename to include the new cache directory |
||
| 260 | View Code Duplication | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
|
| 261 | $this->filename = $cacheDir.'/'.basename($this->source, '.'.$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.png'; |
||
| 262 | } else { |
||
| 263 | $this->filename = $cacheDir.'/'.basename($this->source, '.'.$this->sourceType->getValue()).'_'.$this->width->getValue().'x'.$this->height->getValue().'.jpg'; |
||
| 264 | } |
||
| 265 | } |
||
| 266 | |||
| 267 | self::$logger->debug('Image filename is ['.$this->filename.']'); |
||
| 268 | } |
||
| 269 | |||
| 270 | /** |
||
| 271 | * Gets the auto-generated filename for the image in the /cache directory. |
||
| 272 | * |
||
| 273 | * @since 1.0 |
||
| 274 | */ |
||
| 275 | public function getFilename() |
||
| 279 | |||
| 280 | /** |
||
| 281 | * Renders the actual binary image using GD library calls. |
||
| 282 | * |
||
| 283 | * @since 1.0 |
||
| 284 | */ |
||
| 285 | public function renderImage() |
||
| 286 | { |
||
| 287 | $config = ConfigProvider::getInstance(); |
||
| 288 | |||
| 289 | // if scaled, we need to compute the target image size |
||
| 290 | if ($this->scale->getBooleanValue() && isset($_COOKIE['screenSize'])) { |
||
| 291 | $originalScreenResolution = explode('x', $config->get('sysCMSImagesWidgetScreenResolution')); |
||
| 292 | $originalScreenX = $originalScreenResolution[0]; |
||
| 293 | $originalScreenY = $originalScreenResolution[1]; |
||
| 294 | |||
| 295 | $targetScreenResolution = explode('x', $_COOKIE['screenSize']); |
||
| 296 | $targetScreenX = $targetScreenResolution[0]; |
||
| 297 | $targetScreenY = $targetScreenResolution[1]; |
||
| 298 | |||
| 299 | // calculate the new units we will scale by |
||
| 300 | $xu = $targetScreenX / $originalScreenX; |
||
| 301 | $yu = $targetScreenY / $originalScreenY; |
||
| 302 | |||
| 303 | $this->width = new Integer(intval($this->width->getValue() * $xu)); |
||
| 304 | $this->height = new Integer(intval($this->height->getValue() * $yu)); |
||
| 305 | |||
| 306 | // need to update the cache filename as the dimensions have changed |
||
| 307 | $this->setFilename(); |
||
| 308 | } |
||
| 309 | |||
| 310 | // check the image cache first before we proceed |
||
| 311 | if ($this->checkCache()) { |
||
| 312 | $this->loadCache(); |
||
| 313 | } else { |
||
| 314 | // now get the old image |
||
| 315 | switch ($this->sourceType->getValue()) { |
||
| 316 | case 'gif': |
||
| 317 | $old_image = imagecreatefromgif($this->source); |
||
| 318 | break; |
||
| 319 | case 'jpg': |
||
| 320 | $old_image = imagecreatefromjpeg($this->source); |
||
| 321 | break; |
||
| 322 | case 'png': |
||
| 323 | $old_image = imagecreatefrompng($this->source); |
||
| 324 | break; |
||
| 325 | } |
||
| 326 | |||
| 327 | if (!$old_image) { |
||
| 328 | $im = imagecreatetruecolor($this->width->getValue(), $this->height->getValue()); |
||
| 329 | $bgc = imagecolorallocate($im, 255, 255, 255); |
||
| 330 | $tc = imagecolorallocate($im, 0, 0, 0); |
||
| 331 | imagefilledrectangle($im, 0, 0, $this->width->getValue(), $this->height->getValue(), $bgc); |
||
| 332 | |||
| 333 | imagestring($im, 1, 5, 5, "Error loading $this->source", $tc); |
||
| 334 | View Code Duplication | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
|
| 335 | imagepng($im); |
||
| 336 | } else { |
||
| 337 | imagejpeg($im); |
||
| 338 | } |
||
| 339 | imagedestroy($im); |
||
| 340 | } else { |
||
| 341 | // the dimensions of the source image |
||
| 342 | $oldWidth = imagesx($old_image); |
||
| 343 | $oldHeight = imagesy($old_image); |
||
| 344 | |||
| 345 | // now create the new image |
||
| 346 | $new_image = imagecreatetruecolor($this->width->getValue(), $this->height->getValue()); |
||
| 347 | |||
| 348 | // set a transparent background for PNGs |
||
| 349 | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
||
| 350 | // Turn off transparency blending (temporarily) |
||
| 351 | imagealphablending($new_image, false); |
||
| 352 | |||
| 353 | // Create a new transparent color for image |
||
| 354 | $color = imagecolorallocatealpha($new_image, 255, 0, 0, 0); |
||
| 355 | |||
| 356 | // Completely fill the background of the new image with allocated color. |
||
| 357 | imagefill($new_image, 0, 0, $color); |
||
| 358 | |||
| 359 | // Restore transparency blending |
||
| 360 | imagesavealpha($new_image, true); |
||
| 361 | } |
||
| 362 | // copy the old image to the new image (in memory, not the file!) |
||
| 363 | imagecopyresampled($new_image, $old_image, 0, 0, 0, 0, $this->width->getValue(), $this->height->getValue(), $oldWidth, $oldHeight); |
||
| 364 | |||
| 365 | View Code Duplication | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
|
| 366 | imagepng($new_image); |
||
| 367 | } else { |
||
| 368 | imagejpeg($new_image, null, 100 * $this->quality->getValue()); |
||
| 369 | } |
||
| 370 | |||
| 371 | $this->cache($new_image); |
||
| 372 | imagedestroy($old_image); |
||
| 373 | imagedestroy($new_image); |
||
| 374 | } |
||
| 375 | } |
||
| 376 | } |
||
| 377 | |||
| 378 | /** |
||
| 379 | * Caches the image to the cache directory. |
||
| 380 | * |
||
| 381 | * @param image $image the binary GD image stream to save |
||
| 382 | * |
||
| 383 | * @since 1.0 |
||
| 384 | */ |
||
| 385 | private function cache($image) |
||
| 386 | { |
||
| 387 | $config = ConfigProvider::getInstance(); |
||
| 388 | |||
| 389 | if ($this->sourceType->getValue() == 'png' && $config->get('cms.images.perserve.png')) { |
||
| 390 | imagepng($image, $this->filename); |
||
| 391 | } else { |
||
| 392 | imagejpeg($image, $this->filename, 100 * $this->quality->getValue()); |
||
| 393 | } |
||
| 394 | } |
||
| 395 | |||
| 396 | /** |
||
| 397 | * Used to check the image cache for the image jpeg cache file. |
||
| 398 | * |
||
| 399 | * @return bool |
||
| 400 | * |
||
| 401 | * @since 1.0 |
||
| 402 | */ |
||
| 403 | private function checkCache() |
||
| 407 | |||
| 408 | /** |
||
| 409 | * Method to load the content of the image cache file to the standard output stream (the browser). |
||
| 410 | * |
||
| 411 | * @since 1.0 |
||
| 412 | */ |
||
| 413 | private function loadCache() |
||
| 417 | |||
| 418 | /** |
||
| 419 | * Converts a URL for an image to a relative file system path for the image, assuming it is |
||
| 420 | * hosted on the same server as the application. |
||
| 421 | * |
||
| 422 | * @param string $imgURL |
||
| 423 | * |
||
| 424 | * @return string the path of the image |
||
| 425 | * |
||
| 426 | * @since 1.0 |
||
| 427 | */ |
||
| 428 | public static function convertImageURLToPath($imgURL) |
||
| 436 | } |
||
| 437 |
This check marks private properties in classes that are never used. Those properties can be removed.