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 Swift 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 Swift, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 47 | class Swift extends \OC\Files\Storage\Common { |
||
| 48 | |||
| 49 | /** |
||
| 50 | * @var \OpenCloud\ObjectStore\Service |
||
| 51 | */ |
||
| 52 | private $connection; |
||
| 53 | /** |
||
| 54 | * @var \OpenCloud\ObjectStore\Resource\Container |
||
| 55 | */ |
||
| 56 | private $container; |
||
| 57 | /** |
||
| 58 | * @var \OpenCloud\OpenStack |
||
| 59 | */ |
||
| 60 | private $anchor; |
||
| 61 | /** |
||
| 62 | * @var string |
||
| 63 | */ |
||
| 64 | private $bucket; |
||
| 65 | /** |
||
| 66 | * Connection parameters |
||
| 67 | * |
||
| 68 | * @var array |
||
| 69 | */ |
||
| 70 | private $params; |
||
| 71 | /** |
||
| 72 | * @var array |
||
| 73 | */ |
||
| 74 | private static $tmpFiles = array(); |
||
| 75 | |||
| 76 | /** |
||
| 77 | * Key value cache mapping path to data object. Maps path to |
||
| 78 | * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing |
||
| 79 | * paths and path to false for not existing paths. |
||
| 80 | * @var \OCP\ICache |
||
| 81 | */ |
||
| 82 | private $objectCache; |
||
| 83 | |||
| 84 | /** |
||
| 85 | * @param string $path |
||
| 86 | */ |
||
| 87 | View Code Duplication | private function normalizePath($path) { |
|
| 98 | |||
| 99 | const SUBCONTAINER_FILE = '.subcontainers'; |
||
| 100 | |||
| 101 | /** |
||
| 102 | * translate directory path to container name |
||
| 103 | * |
||
| 104 | * @param string $path |
||
| 105 | * @return string |
||
| 106 | */ |
||
| 107 | |||
| 108 | /** |
||
| 109 | * Fetches an object from the API. |
||
| 110 | * If the object is cached already or a |
||
| 111 | * failed "doesn't exist" response was cached, |
||
| 112 | * that one will be returned. |
||
| 113 | * |
||
| 114 | * @param string $path |
||
| 115 | * @return \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject|bool object |
||
| 116 | * or false if the object did not exist |
||
| 117 | */ |
||
| 118 | private function fetchObject($path) { |
||
| 119 | if ($this->objectCache->hasKey($path)) { |
||
| 120 | // might be "false" if object did not exist from last check |
||
| 121 | return $this->objectCache->get($path); |
||
| 122 | } |
||
| 123 | try { |
||
| 124 | $object = $this->getContainer()->getPartialObject($path); |
||
| 125 | $this->objectCache->set($path, $object); |
||
| 126 | return $object; |
||
|
|
|||
| 127 | } catch (ClientErrorResponseException $e) { |
||
| 128 | // this exception happens when the object does not exist, which |
||
| 129 | // is expected in most cases |
||
| 130 | $this->objectCache->set($path, false); |
||
| 131 | return false; |
||
| 132 | } catch (ClientErrorResponseException $e) { |
||
| 133 | // Expected response is "404 Not Found", so only log if it isn't |
||
| 134 | if ($e->getResponse()->getStatusCode() !== 404) { |
||
| 135 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 136 | } |
||
| 137 | return false; |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | /** |
||
| 142 | * Returns whether the given path exists. |
||
| 143 | * |
||
| 144 | * @param string $path |
||
| 145 | * |
||
| 146 | * @return bool true if the object exist, false otherwise |
||
| 147 | */ |
||
| 148 | private function doesObjectExist($path) { |
||
| 149 | return $this->fetchObject($path) !== false; |
||
| 150 | } |
||
| 151 | |||
| 152 | public function __construct($params) { |
||
| 153 | if ((empty($params['key']) and empty($params['password'])) |
||
| 154 | or empty($params['user']) or empty($params['bucket']) |
||
| 155 | or empty($params['region']) |
||
| 156 | ) { |
||
| 157 | throw new \Exception("API Key or password, Username, Bucket and Region have to be configured."); |
||
| 158 | } |
||
| 159 | |||
| 160 | $this->id = 'swift::' . $params['user'] . md5($params['bucket']); |
||
| 161 | |||
| 162 | $bucketUrl = Url::factory($params['bucket']); |
||
| 163 | if ($bucketUrl->isAbsolute()) { |
||
| 164 | $this->bucket = end(($bucketUrl->getPathSegments())); |
||
| 165 | $params['endpoint_url'] = $bucketUrl->addPath('..')->normalizePath(); |
||
| 166 | } else { |
||
| 167 | $this->bucket = $params['bucket']; |
||
| 168 | } |
||
| 169 | |||
| 170 | if (empty($params['url'])) { |
||
| 171 | $params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/'; |
||
| 172 | } |
||
| 173 | |||
| 174 | if (empty($params['service_name'])) { |
||
| 175 | $params['service_name'] = 'cloudFiles'; |
||
| 176 | } |
||
| 177 | |||
| 178 | $this->params = $params; |
||
| 179 | // FIXME: private class... |
||
| 180 | $this->objectCache = new \OC\Cache\CappedMemoryCache(); |
||
| 181 | } |
||
| 182 | |||
| 183 | public function mkdir($path) { |
||
| 184 | $path = $this->normalizePath($path); |
||
| 185 | |||
| 186 | if ($this->is_dir($path)) { |
||
| 187 | return false; |
||
| 188 | } |
||
| 189 | |||
| 190 | if ($path !== '.') { |
||
| 191 | $path .= '/'; |
||
| 192 | } |
||
| 193 | |||
| 194 | try { |
||
| 195 | $customHeaders = array('content-type' => 'httpd/unix-directory'); |
||
| 196 | $metadataHeaders = DataObject::stockHeaders(array()); |
||
| 197 | $allHeaders = $customHeaders + $metadataHeaders; |
||
| 198 | $this->getContainer()->uploadObject($path, '', $allHeaders); |
||
| 199 | // invalidate so that the next access gets the real object |
||
| 200 | // with all properties |
||
| 201 | $this->objectCache->remove($path); |
||
| 202 | } catch (Exceptions\CreateUpdateError $e) { |
||
| 203 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 204 | return false; |
||
| 205 | } |
||
| 206 | |||
| 207 | return true; |
||
| 208 | } |
||
| 209 | |||
| 210 | public function file_exists($path) { |
||
| 219 | |||
| 220 | public function rmdir($path) { |
||
| 221 | $path = $this->normalizePath($path); |
||
| 222 | |||
| 223 | if (!$this->is_dir($path) || !$this->isDeletable($path)) { |
||
| 224 | return false; |
||
| 225 | } |
||
| 226 | |||
| 227 | $dh = $this->opendir($path); |
||
| 228 | View Code Duplication | while ($file = readdir($dh)) { |
|
| 229 | if (\OC\Files\Filesystem::isIgnoredDir($file)) { |
||
| 230 | continue; |
||
| 231 | } |
||
| 232 | |||
| 233 | if ($this->is_dir($path . '/' . $file)) { |
||
| 234 | $this->rmdir($path . '/' . $file); |
||
| 235 | } else { |
||
| 236 | $this->unlink($path . '/' . $file); |
||
| 237 | } |
||
| 238 | } |
||
| 239 | |||
| 240 | try { |
||
| 241 | $this->getContainer()->dataObject()->setName($path . '/')->delete(); |
||
| 242 | $this->objectCache->remove($path . '/'); |
||
| 243 | } catch (Exceptions\DeleteError $e) { |
||
| 244 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 245 | return false; |
||
| 246 | } |
||
| 247 | |||
| 248 | return true; |
||
| 249 | } |
||
| 250 | |||
| 251 | public function opendir($path) { |
||
| 252 | $path = $this->normalizePath($path); |
||
| 253 | |||
| 254 | if ($path === '.') { |
||
| 255 | $path = ''; |
||
| 256 | } else { |
||
| 257 | $path .= '/'; |
||
| 258 | } |
||
| 259 | |||
| 260 | $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of # |
||
| 261 | |||
| 262 | try { |
||
| 263 | $files = array(); |
||
| 264 | /** @var OpenCloud\Common\Collection $objects */ |
||
| 265 | $objects = $this->getContainer()->objectList(array( |
||
| 266 | 'prefix' => $path, |
||
| 267 | 'delimiter' => '/' |
||
| 268 | )); |
||
| 269 | |||
| 270 | /** @var OpenCloud\ObjectStore\Resource\DataObject $object */ |
||
| 271 | foreach ($objects as $object) { |
||
| 272 | $file = basename($object->getName()); |
||
| 273 | if ($file !== basename($path)) { |
||
| 274 | $files[] = $file; |
||
| 275 | } |
||
| 276 | } |
||
| 277 | |||
| 278 | return IteratorDirectory::wrap($files); |
||
| 279 | } catch (\Exception $e) { |
||
| 280 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 281 | return false; |
||
| 282 | } |
||
| 283 | |||
| 284 | } |
||
| 285 | |||
| 286 | public function stat($path) { |
||
| 287 | $path = $this->normalizePath($path); |
||
| 288 | |||
| 289 | if ($path === '.') { |
||
| 290 | $path = ''; |
||
| 291 | } else if ($this->is_dir($path)) { |
||
| 292 | $path .= '/'; |
||
| 293 | } |
||
| 294 | |||
| 295 | try { |
||
| 296 | /** @var DataObject $object */ |
||
| 297 | $object = $this->fetchObject($path); |
||
| 298 | if (!$object) { |
||
| 299 | return false; |
||
| 300 | } |
||
| 301 | } catch (ClientErrorResponseException $e) { |
||
| 302 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 303 | return false; |
||
| 304 | } |
||
| 305 | |||
| 306 | $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->getLastModified()); |
||
| 307 | if ($dateTime !== false) { |
||
| 308 | $mtime = $dateTime->getTimestamp(); |
||
| 309 | } else { |
||
| 310 | $mtime = null; |
||
| 311 | } |
||
| 312 | $objectMetadata = $object->getMetadata(); |
||
| 313 | $metaTimestamp = $objectMetadata->getProperty('timestamp'); |
||
| 314 | if (isset($metaTimestamp)) { |
||
| 315 | $mtime = $metaTimestamp; |
||
| 316 | } |
||
| 317 | |||
| 318 | if (!empty($mtime)) { |
||
| 319 | $mtime = floor($mtime); |
||
| 320 | } |
||
| 321 | |||
| 322 | $stat = array(); |
||
| 323 | $stat['size'] = (int)$object->getContentLength(); |
||
| 324 | $stat['mtime'] = $mtime; |
||
| 325 | $stat['atime'] = time(); |
||
| 326 | return $stat; |
||
| 327 | } |
||
| 328 | |||
| 329 | public function filetype($path) { |
||
| 344 | |||
| 345 | public function unlink($path) { |
||
| 346 | $path = $this->normalizePath($path); |
||
| 347 | |||
| 348 | if ($this->is_dir($path)) { |
||
| 349 | return $this->rmdir($path); |
||
| 350 | } |
||
| 351 | |||
| 352 | try { |
||
| 353 | $this->getContainer()->dataObject()->setName($path)->delete(); |
||
| 354 | $this->objectCache->remove($path); |
||
| 355 | $this->objectCache->remove($path . '/'); |
||
| 356 | } catch (ClientErrorResponseException $e) { |
||
| 357 | if ($e->getResponse()->getStatusCode() !== 404) { |
||
| 358 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 359 | } |
||
| 360 | return false; |
||
| 361 | } |
||
| 362 | |||
| 363 | return true; |
||
| 364 | } |
||
| 365 | |||
| 366 | public function fopen($path, $mode) { |
||
| 367 | $path = $this->normalizePath($path); |
||
| 368 | |||
| 369 | switch ($mode) { |
||
| 370 | case 'r': |
||
| 371 | case 'rb': |
||
| 372 | try { |
||
| 373 | $c = $this->getContainer(); |
||
| 374 | $streamFactory = new \Guzzle\Stream\PhpStreamRequestFactory(); |
||
| 375 | $streamInterface = $streamFactory->fromRequest( |
||
| 376 | $c->getClient() |
||
| 377 | ->get($c->getUrl($path))); |
||
| 378 | $streamInterface->rewind(); |
||
| 379 | $stream = $streamInterface->getStream(); |
||
| 380 | stream_context_set_option($stream, 'swift','content', $streamInterface); |
||
| 381 | if(!strrpos($streamInterface |
||
| 382 | ->getMetaData('wrapper_data')[0], '404 Not Found')) { |
||
| 383 | return $stream; |
||
| 384 | } |
||
| 385 | return false; |
||
| 386 | } catch (\Guzzle\Http\Exception\BadResponseException $e) { |
||
| 387 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 388 | return false; |
||
| 389 | } |
||
| 390 | case 'w': |
||
| 391 | case 'wb': |
||
| 392 | case 'a': |
||
| 393 | case 'ab': |
||
| 394 | case 'r+': |
||
| 395 | case 'w+': |
||
| 396 | case 'wb+': |
||
| 397 | case 'a+': |
||
| 398 | case 'x': |
||
| 399 | case 'x+': |
||
| 400 | case 'c': |
||
| 401 | case 'c+': |
||
| 402 | if (strrpos($path, '.') !== false) { |
||
| 403 | $ext = substr($path, strrpos($path, '.')); |
||
| 404 | } else { |
||
| 405 | $ext = ''; |
||
| 406 | } |
||
| 407 | $tmpFile = \OCP\Files::tmpFile($ext); |
||
| 408 | \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack')); |
||
| 409 | // Fetch existing file if required |
||
| 410 | if ($mode[0] !== 'w' && $this->file_exists($path)) { |
||
| 411 | if ($mode[0] === 'x') { |
||
| 412 | // File cannot already exist |
||
| 413 | return false; |
||
| 414 | } |
||
| 415 | $source = $this->fopen($path, 'r'); |
||
| 416 | file_put_contents($tmpFile, $source); |
||
| 417 | // Seek to end if required |
||
| 418 | if ($mode[0] === 'a') { |
||
| 419 | fseek($tmpFile, 0, SEEK_END); |
||
| 420 | } |
||
| 421 | } |
||
| 422 | self::$tmpFiles[$tmpFile] = $path; |
||
| 423 | |||
| 424 | return fopen('close://' . $tmpFile, $mode); |
||
| 425 | } |
||
| 426 | } |
||
| 427 | |||
| 428 | public function touch($path, $mtime = null) { |
||
| 429 | $path = $this->normalizePath($path); |
||
| 430 | if (is_null($mtime)) { |
||
| 431 | $mtime = time(); |
||
| 432 | } |
||
| 433 | $metadata = array('timestamp' => $mtime); |
||
| 434 | if ($this->file_exists($path)) { |
||
| 435 | if ($this->is_dir($path) && $path != '.') { |
||
| 436 | $path .= '/'; |
||
| 437 | } |
||
| 438 | |||
| 439 | $object = $this->fetchObject($path); |
||
| 440 | if ($object->saveMetadata($metadata)) { |
||
| 441 | // invalidate target object to force repopulation on fetch |
||
| 442 | $this->objectCache->remove($path); |
||
| 443 | } |
||
| 444 | return true; |
||
| 445 | } else { |
||
| 446 | $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path); |
||
| 447 | $customHeaders = array('content-type' => $mimeType); |
||
| 448 | $metadataHeaders = DataObject::stockHeaders($metadata); |
||
| 449 | $allHeaders = $customHeaders + $metadataHeaders; |
||
| 450 | $this->getContainer()->uploadObject($path, '', $allHeaders); |
||
| 451 | // invalidate target object to force repopulation on fetch |
||
| 452 | $this->objectCache->remove($path); |
||
| 453 | return true; |
||
| 454 | } |
||
| 455 | } |
||
| 456 | |||
| 457 | public function copy($path1, $path2) { |
||
| 458 | $path1 = $this->normalizePath($path1); |
||
| 459 | $path2 = $this->normalizePath($path2); |
||
| 460 | |||
| 461 | $fileType = $this->filetype($path1); |
||
| 462 | if ($fileType === 'file') { |
||
| 463 | |||
| 464 | // make way |
||
| 465 | $this->unlink($path2); |
||
| 466 | |||
| 467 | try { |
||
| 468 | $source = $this->fetchObject($path1); |
||
| 469 | $source->copy($this->bucket . '/' . $path2); |
||
| 470 | // invalidate target object to force repopulation on fetch |
||
| 471 | $this->objectCache->remove($path2); |
||
| 472 | $this->objectCache->remove($path2 . '/'); |
||
| 473 | } catch (ClientErrorResponseException $e) { |
||
| 474 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 475 | return false; |
||
| 476 | } |
||
| 477 | |||
| 478 | } else if ($fileType === 'dir') { |
||
| 479 | |||
| 480 | // make way |
||
| 481 | $this->unlink($path2); |
||
| 482 | |||
| 483 | try { |
||
| 484 | $source = $this->fetchObject($path1 . '/'); |
||
| 485 | $source->copy($this->bucket . '/' . $path2 . '/'); |
||
| 486 | // invalidate target object to force repopulation on fetch |
||
| 487 | $this->objectCache->remove($path2); |
||
| 488 | $this->objectCache->remove($path2 . '/'); |
||
| 489 | } catch (ClientErrorResponseException $e) { |
||
| 490 | \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR); |
||
| 491 | return false; |
||
| 492 | } |
||
| 493 | |||
| 494 | $dh = $this->opendir($path1); |
||
| 495 | while ($file = readdir($dh)) { |
||
| 496 | if (\OC\Files\Filesystem::isIgnoredDir($file)) { |
||
| 497 | continue; |
||
| 498 | } |
||
| 499 | |||
| 500 | $source = $path1 . '/' . $file; |
||
| 501 | $target = $path2 . '/' . $file; |
||
| 502 | $this->copy($source, $target); |
||
| 503 | } |
||
| 504 | |||
| 505 | } else { |
||
| 506 | //file does not exist |
||
| 507 | return false; |
||
| 508 | } |
||
| 509 | |||
| 510 | return true; |
||
| 511 | } |
||
| 512 | |||
| 513 | public function rename($path1, $path2) { |
||
| 536 | |||
| 537 | public function getId() { |
||
| 540 | |||
| 541 | /** |
||
| 542 | * Returns the connection |
||
| 543 | * |
||
| 544 | * @return OpenCloud\ObjectStore\Service connected client |
||
| 545 | * @throws \Exception if connection could not be made |
||
| 546 | */ |
||
| 547 | public function getConnection() { |
||
| 589 | |||
| 590 | /** |
||
| 591 | * Returns the initialized object store container. |
||
| 592 | * |
||
| 593 | * @return OpenCloud\ObjectStore\Resource\Container |
||
| 594 | */ |
||
| 595 | public function getContainer() { |
||
| 612 | |||
| 613 | public function writeBack($tmpFile) { |
||
| 614 | if (!isset(self::$tmpFiles[$tmpFile])) { |
||
| 615 | return false; |
||
| 616 | } |
||
| 617 | $fileData = fopen($tmpFile, 'r'); |
||
| 618 | $this->getContainer()->uploadObject(self::$tmpFiles[$tmpFile], $fileData); |
||
| 619 | // invalidate target object to force repopulation on fetch |
||
| 620 | $this->objectCache->remove(self::$tmpFiles[$tmpFile]); |
||
| 621 | unlink($tmpFile); |
||
| 622 | } |
||
| 623 | |||
| 624 | public function hasUpdated($path, $time) { |
||
| 645 | |||
| 646 | /** |
||
| 647 | * check if curl is installed |
||
| 648 | */ |
||
| 649 | public static function checkDependencies() { |
||
| 652 | |||
| 653 | } |
||
| 654 |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_functionexpects aPostobject, and outputs the author of the post. The base classPostreturns a simple string and outputting a simple string will work just fine. However, the child classBlogPostwhich is a sub-type ofPostinstead decided to return anobject, and is therefore violating the SOLID principles. If aBlogPostwere passed tomy_function, PHP would not complain, but ultimately fail when executing thestrtouppercall in its body.