Complex classes like FileAdapterStrategy 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 FileAdapterStrategy, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 64 | class FileAdapterStrategy extends AbstractAdapterStrategy |
||
| 65 | { |
||
| 66 | /** |
||
| 67 | * Adapter strategy type |
||
| 68 | * |
||
| 69 | * @var string |
||
| 70 | */ |
||
| 71 | const TYPE = 'file'; |
||
| 72 | /** |
||
| 73 | * Configuration |
||
| 74 | * |
||
| 75 | * @var array |
||
| 76 | */ |
||
| 77 | protected $config = null; |
||
| 78 | /** |
||
| 79 | * Root directory (without trailing directory separator) |
||
| 80 | * |
||
| 81 | * @var string |
||
| 82 | */ |
||
| 83 | protected $root = null; |
||
| 84 | /** |
||
| 85 | * Configuration directory (including trailing directory separator) |
||
| 86 | * |
||
| 87 | * @var string |
||
| 88 | */ |
||
| 89 | protected $configDir = null; |
||
| 90 | /** |
||
| 91 | * Glob visibilities |
||
| 92 | * |
||
| 93 | * @var array |
||
| 94 | */ |
||
| 95 | protected static $globVisibilities = [ |
||
| 96 | SelectorInterface::VISIBLE => '', |
||
| 97 | SelectorInterface::HIDDEN => '.', |
||
| 98 | SelectorInterface::ALL => '{.,}', |
||
| 99 | ]; |
||
| 100 | |||
| 101 | /** |
||
| 102 | * Adapter strategy constructor |
||
| 103 | * |
||
| 104 | * @param array $config Adapter strategy configuration |
||
| 105 | * @throws InvalidArgumentException If the root directory configuration is empty |
||
| 106 | * @throws InvalidArgumentException If the root directory configuration is invalid |
||
| 107 | */ |
||
| 108 | 20 | public function __construct(array $config) |
|
| 109 | { |
||
| 110 | 20 | parent::__construct($config, ['root']); |
|
| 111 | |||
| 112 | // If the root directory configuration is empty |
||
| 113 | 19 | if (empty($this->config['root'])) { |
|
| 114 | 1 | throw new InvalidArgumentException( |
|
| 115 | 1 | 'Empty file adapter strategy root', |
|
| 116 | 1 | InvalidArgumentException::EMTPY_FILE_STRATEGY_ROOT |
|
| 117 | ); |
||
| 118 | } |
||
| 119 | |||
| 120 | // Get the real path of the root directory |
||
| 121 | 18 | $this->root = realpath($this->config['root']); |
|
| 122 | |||
| 123 | // If the repository should be initialized |
||
| 124 | 18 | if (!empty($this->config['init']) |
|
| 125 | 18 | && (boolean)$this->config['init'] |
|
| 126 | 18 | && $this->initializeRepository() |
|
| 127 | ) { |
||
| 128 | 6 | $this->root = realpath($this->config['root']); |
|
| 129 | } |
||
| 130 | |||
| 131 | // If the root directory configuration is still invalid |
||
| 132 | 16 | if (empty($this->root) || !@is_dir($this->root)) { |
|
| 133 | 1 | throw new InvalidArgumentException( |
|
| 134 | sprintf( |
||
| 135 | 1 | 'Invalid file adapter strategy root "%s"', |
|
| 136 | 1 | $this->config['root'] |
|
| 137 | ), |
||
| 138 | 1 | InvalidArgumentException::INVALID_FILE_STRATEGY_ROOT |
|
| 139 | ); |
||
| 140 | } |
||
| 141 | |||
| 142 | 15 | $this->configDir = $this->root.DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR; |
|
| 143 | 15 | } |
|
| 144 | |||
| 145 | /** |
||
| 146 | * Initialize the repository |
||
| 147 | * |
||
| 148 | * @return boolean Success |
||
| 149 | * @throws DomainRepositoryRuntimeException If the repository cannot be initialized |
||
| 150 | * @throws DomainRepositoryRuntimeException If the repository size descriptor can not be created |
||
| 151 | */ |
||
| 152 | 8 | public function initializeRepository() |
|
| 153 | { |
||
| 154 | // Successively create the repository directories |
||
| 155 | 8 | $repoDirectories = [$this->config['root'], $this->config['root'].DIRECTORY_SEPARATOR.'.repo']; |
|
| 156 | 8 | foreach ($repoDirectories as $repoDirectory) { |
|
| 157 | // If the repository cannot be initialized |
||
| 158 | 8 | if (file_exists($repoDirectory) ? !is_dir($repoDirectory) : !mkdir($repoDirectory, 0777, true)) { |
|
| 159 | 8 | throw new DomainRepositoryRuntimeException('Could not initialize repository', DomainRepositoryRuntimeException::REPO_NOT_INITIALIZED); |
|
| 160 | } |
||
| 161 | } |
||
| 162 | |||
| 163 | // If the repository size descriptor can not be created |
||
| 164 | 7 | $configDir = $this->config['root'].DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR; |
|
| 165 | 7 | if ((file_exists($configDir.'size.txt') && !is_file($configDir.'size.txt')) |
|
| 166 | 7 | || !file_put_contents($configDir.'size.txt', '0') |
|
| 167 | ) { |
||
| 168 | 1 | throw new DomainRepositoryRuntimeException( |
|
| 169 | 1 | 'Could not create repository size descriptor', |
|
| 170 | 1 | DomainRepositoryRuntimeException::REPO_SIZE_DESCRIPTOR_NOT_CREATED |
|
| 171 | ); |
||
| 172 | } |
||
| 173 | |||
| 174 | 6 | return true; |
|
| 175 | } |
||
| 176 | |||
| 177 | /** |
||
| 178 | * Find objects by selector |
||
| 179 | * |
||
| 180 | * @param Selector|SelectorInterface $selector Object selector |
||
| 181 | * @param RepositoryInterface $repository Object repository |
||
| 182 | * @return PathInterface[] Object paths |
||
| 183 | */ |
||
| 184 | 7 | public function findObjectPaths(SelectorInterface $selector, RepositoryInterface $repository) |
|
| 185 | { |
||
| 186 | 7 | chdir($this->root); |
|
| 187 | |||
| 188 | // Build a glob string from the selector |
||
| 189 | 7 | $glob = ''; |
|
| 190 | 7 | $globFlags = GLOB_ONLYDIR | GLOB_NOSORT; |
|
| 191 | |||
| 192 | 7 | $year = $selector->getYear(); |
|
| 193 | 7 | if ($year !== null) { |
|
| 194 | 7 | $glob .= '/'.$year; |
|
| 195 | } |
||
| 196 | |||
| 197 | 7 | $month = $selector->getMonth(); |
|
| 198 | 7 | if ($month !== null) { |
|
| 199 | 7 | $glob .= '/'.$month; |
|
| 200 | } |
||
| 201 | |||
| 202 | 7 | $day = $selector->getDay(); |
|
| 203 | 7 | if ($day !== null) { |
|
| 204 | 7 | $glob .= '/'.$day; |
|
| 205 | } |
||
| 206 | |||
| 207 | 7 | $hour = $selector->getHour(); |
|
| 208 | 7 | if ($hour !== null) { |
|
| 209 | 2 | $glob .= '/'.$hour; |
|
| 210 | } |
||
| 211 | |||
| 212 | 7 | $minute = $selector->getMinute(); |
|
| 213 | 7 | if ($minute !== null) { |
|
| 214 | 2 | $glob .= '/'.$minute; |
|
| 215 | } |
||
| 216 | |||
| 217 | 7 | $second = $selector->getSecond(); |
|
| 218 | 7 | if ($second !== null) { |
|
| 219 | 2 | $glob .= '/'.$second; |
|
| 220 | } |
||
| 221 | |||
| 222 | 7 | $visibility = $selector->getVisibility(); |
|
| 223 | 7 | $uid = $selector->getId(); |
|
| 224 | 7 | $type = $selector->getType(); |
|
| 225 | 7 | if (($uid !== null) || ($type !== null)) { |
|
| 226 | 7 | $glob .= '/'.($uid ?: SelectorInterface::WILDCARD).'-'.($type ?: SelectorInterface::WILDCARD); |
|
| 227 | |||
| 228 | 7 | $revision = $selector->getRevision(); |
|
| 229 | 7 | if ($revision !== null) { |
|
| 230 | 1 | $glob .= '/'.self::$globVisibilities[$visibility].($uid ?: SelectorInterface::WILDCARD).'-'.$revision; |
|
| 231 | 1 | $globFlags &= ~GLOB_ONLYDIR; |
|
| 232 | } |
||
| 233 | } |
||
| 234 | |||
| 235 | 7 | return array_map( |
|
| 236 | 7 | function ($objectPath) use ($repository) { |
|
| 237 | 7 | return Kernel::create(RepositoryPath::class, [$repository, '/'.$objectPath]); |
|
| 238 | 7 | }, |
|
| 239 | 7 | glob(ltrim($glob, '/'), $globFlags) |
|
| 240 | ); |
||
| 241 | } |
||
| 242 | |||
| 243 | /** |
||
| 244 | * Test if an object resource exists |
||
| 245 | * |
||
| 246 | * @param string $resourcePath Repository relative resource path |
||
| 247 | * @return boolean Object resource exists |
||
| 248 | */ |
||
| 249 | 28 | public function hasResource($resourcePath) |
|
| 253 | |||
| 254 | /** |
||
| 255 | * Return an individual hash for a resource |
||
| 256 | * |
||
| 257 | * @param string $resourcePath Repository relative resource path |
||
| 258 | * @return string|null Resource hash |
||
| 259 | */ |
||
| 260 | 1 | public function getResourceHash($resourcePath) |
|
| 261 | { |
||
| 262 | 1 | return $this->hasResource($this->root.$resourcePath) ? File::hash($this->root.$resourcePath) : null; |
|
| 263 | } |
||
| 264 | |||
| 265 | /** |
||
| 266 | * Import a resource into this repository |
||
| 267 | * |
||
| 268 | * @param string $source Source resource |
||
| 269 | * @param string $target Repository relative target resource path |
||
| 270 | * @return boolean Success |
||
| 271 | */ |
||
| 272 | 1 | public function importResource($source, $target) |
|
| 276 | |||
| 277 | /** |
||
| 278 | * Find and return an object resource |
||
| 279 | * |
||
| 280 | * @param string $resourcePath Repository relative resource path |
||
| 281 | * @return ResourceInterface Object resource |
||
| 282 | */ |
||
| 283 | 26 | public function getObjectResource($resourcePath) |
|
| 287 | |||
| 288 | /** |
||
| 289 | * Allocate an object ID and create an object resource |
||
| 290 | * |
||
| 291 | * @param \Closure $creator Object creation closure |
||
| 292 | * @return ObjectInterface Object |
||
| 293 | * @throws DomainRepositoryRuntimeException If no object could be created |
||
| 294 | * @throws \Exception If another error occurs |
||
| 295 | */ |
||
| 296 | 5 | public function createObjectResource(\Closure $creator) |
|
| 347 | |||
| 348 | /** |
||
| 349 | * Persist an object in the repository |
||
| 350 | * |
||
| 351 | * @param ObjectInterface $object Object |
||
| 352 | * @return AdapterStrategyInterface Self reference |
||
| 353 | */ |
||
| 354 | 4 | public function persistObject(ObjectInterface $object) |
|
| 372 | |||
| 373 | /** |
||
| 374 | * Publish an object in the repository |
||
| 375 | * |
||
| 376 | * @param ObjectInterface $object |
||
| 377 | */ |
||
| 378 | 2 | protected function publishObject(ObjectInterface $object) |
|
| 379 | { |
||
| 380 | 2 | $objectRepositoryPath = $object->getRepositoryPath(); |
|
| 381 | |||
| 382 | // If the object had been persisted as a draft: Remove the draft resource |
||
| 383 | 2 | $objectDraftPath = $objectRepositoryPath->setRevision($object->getRevision()->setDraft(true)); |
|
| 384 | 2 | $absObjectDraftPath = $this->getAbsoluteResourcePath($objectDraftPath); |
|
| 385 | 2 | if (@file_exists($absObjectDraftPath)) { |
|
| 386 | 2 | unlink($absObjectDraftPath); |
|
| 387 | } |
||
| 388 | |||
| 389 | // If it's not the first object revision: Rotate the previous revision resource |
||
| 390 | 2 | $objectRevisionNumber = $object->getRevision()->getRevision(); |
|
| 391 | 2 | if ($objectRevisionNumber > 1) { |
|
| 392 | // Build the "current" object repository path |
||
| 393 | 2 | $currentRevision = Revision::current(); |
|
| 394 | $curObjectResPath = |
||
| 395 | 2 | $this->getAbsoluteResourcePath($objectRepositoryPath->setRevision($currentRevision)); |
|
| 396 | |||
| 397 | // Build the previous object repository path |
||
| 398 | /** @var Revision $previousRevision */ |
||
| 399 | 2 | $previousRevision = Kernel::create(Revision::class, [$objectRevisionNumber - 1]); |
|
| 400 | $prevObjectResPath |
||
| 401 | 2 | = $this->getAbsoluteResourcePath($objectRepositoryPath->setRevision($previousRevision)); |
|
| 402 | |||
| 403 | // Rotate the previous revision's resource path |
||
| 404 | 2 | if (file_exists($curObjectResPath)) { |
|
| 405 | 2 | rename($curObjectResPath, $prevObjectResPath); |
|
| 406 | } |
||
| 407 | } |
||
| 408 | 2 | } |
|
| 409 | |||
| 410 | /** |
||
| 411 | * Build an absolute repository resource path |
||
| 412 | * |
||
| 413 | * @param RepositoryPathInterface $repositoryPath Repository path |
||
| 414 | * @return string Absolute repository resource path |
||
| 415 | */ |
||
| 416 | 4 | public function getAbsoluteResourcePath(RepositoryPathInterface $repositoryPath) |
|
| 417 | { |
||
| 418 | 4 | return $this->root.str_replace( |
|
| 419 | 4 | '/', |
|
| 420 | 4 | DIRECTORY_SEPARATOR, |
|
| 421 | 4 | $repositoryPath->withExtension(getenv('OBJECT_RESOURCE_EXTENSION')) |
|
| 422 | ); |
||
| 423 | } |
||
| 424 | |||
| 425 | /** |
||
| 426 | * Persist an object resource in the repository |
||
| 427 | * |
||
| 428 | * @param ObjectInterface $object Object |
||
| 429 | * @return AdapterStrategyInterface Self reference |
||
| 430 | */ |
||
| 431 | 4 | protected function persistObjectResource(ObjectInterface $object) |
|
| 448 | |||
| 449 | /** |
||
| 450 | * Return the repository size (number of objects in the repository) |
||
| 451 | * |
||
| 452 | * @return int Repository size |
||
| 453 | */ |
||
| 454 | 5 | public function getRepositorySize() |
|
| 463 | |||
| 464 | /** |
||
| 465 | * Delete all revisions of an object |
||
| 466 | * |
||
| 467 | * @param ObjectInterface $object Object |
||
| 468 | * @return ObjectInterface Object |
||
| 469 | */ |
||
| 470 | 3 | protected function deleteObject(ObjectInterface $object) |
|
| 495 | |||
| 496 | /** |
||
| 497 | * Undelete all revisions of an object |
||
| 498 | * |
||
| 499 | * @param ObjectInterface $object Object |
||
| 500 | * @return ObjectInterface Object |
||
| 501 | */ |
||
| 502 | 2 | protected function undeleteObject(ObjectInterface $object) |
|
| 527 | } |
||
| 528 |
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.