Complex classes like Hydrator 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 Hydrator, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 11 | class Hydrator implements ConfigurableProcessor |
||
| 12 | { |
||
| 13 | use \Karma\Logging\LoggerAware; |
||
| 14 | |||
| 15 | private const |
||
| 16 | TODO_VALUE = '__TODO__', |
||
| 17 | FIXME_VALUE = '__FIXME__', |
||
| 18 | VARIABLE_REGEX = '~<%(?P<variableName>[A-Za-z0-9_\.\-]+)%>~'; |
||
| 19 | |||
| 20 | private Filesystem |
||
|
|
|||
| 21 | $sources, |
||
| 22 | $target; |
||
| 23 | private Configuration |
||
| 24 | $reader; |
||
| 25 | private Finder |
||
| 26 | $finder; |
||
| 27 | private string |
||
| 28 | $suffix; |
||
| 29 | private bool |
||
| 30 | $dryRun, |
||
| 31 | $enableBackup, |
||
| 32 | $nonDistFilesOverwriteAllowed; |
||
| 33 | private FormatterProvider |
||
| 34 | $formatterProvider; |
||
| 35 | private ?string |
||
| 36 | $currentFormatterName, |
||
| 37 | $currentTargetFile, |
||
| 38 | $systemEnvironment; |
||
| 39 | private array |
||
| 40 | $unusedVariables, |
||
| 41 | $unvaluedVariables, |
||
| 42 | $hydratedFiles; |
||
| 43 | |||
| 44 | 87 | public function __construct(Filesystem $sources, Filesystem $target, Configuration $reader, Finder $finder, FormatterProvider $formatterProvider = null) |
|
| 45 | { |
||
| 46 | 87 | $this->logger = new NullLogger(); |
|
| 47 | |||
| 48 | 87 | $this->sources = $sources; |
|
| 49 | 87 | $this->target = $target; |
|
| 50 | 87 | $this->reader = $reader; |
|
| 51 | 87 | $this->finder = $finder; |
|
| 52 | |||
| 53 | 87 | $this->suffix = Application::DEFAULT_DISTFILE_SUFFIX; |
|
| 54 | 87 | $this->dryRun = false; |
|
| 55 | 87 | $this->enableBackup = false; |
|
| 56 | 87 | $this->nonDistFilesOverwriteAllowed = false; |
|
| 57 | |||
| 58 | 87 | $this->formatterProvider = $formatterProvider ?? new NullProvider(); |
|
| 59 | |||
| 60 | 87 | $this->currentFormatterName = null; |
|
| 61 | 87 | $this->currentTargetFile = null; |
|
| 62 | 87 | $this->systemEnvironment = null; |
|
| 63 | 87 | $this->unusedVariables = array_flip($reader->getAllVariables()); |
|
| 64 | 87 | $this->unvaluedVariables = []; |
|
| 65 | 87 | $this->hydratedFiles = []; |
|
| 66 | 87 | } |
|
| 67 | |||
| 68 | 87 | public function setSuffix(string $suffix): ConfigurableProcessor |
|
| 69 | { |
||
| 70 | 87 | $this->suffix = $suffix; |
|
| 71 | |||
| 72 | 87 | return $this; |
|
| 73 | } |
||
| 74 | |||
| 75 | 2 | public function setDryRun(bool $value = true): ConfigurableProcessor |
|
| 76 | { |
||
| 77 | 2 | $this->dryRun = $value; |
|
| 78 | |||
| 79 | 2 | return $this; |
|
| 80 | } |
||
| 81 | |||
| 82 | 1 | public function enableBackup(bool $value = true): ConfigurableProcessor |
|
| 83 | { |
||
| 84 | 1 | $this->enableBackup = $value; |
|
| 85 | |||
| 86 | 1 | return $this; |
|
| 87 | } |
||
| 88 | |||
| 89 | 15 | public function allowNonDistFilesOverwrite(bool $nonDistFilesOverwriteAllowed = true): ConfigurableProcessor |
|
| 90 | { |
||
| 91 | 15 | $this->nonDistFilesOverwriteAllowed = $nonDistFilesOverwriteAllowed; |
|
| 92 | |||
| 93 | 15 | return $this; |
|
| 94 | } |
||
| 95 | |||
| 96 | 3 | public function setFormatterProvider(FormatterProvider $formatterProvider): ConfigurableProcessor |
|
| 97 | { |
||
| 98 | 3 | $this->formatterProvider = $formatterProvider; |
|
| 99 | |||
| 100 | 3 | return $this; |
|
| 101 | } |
||
| 102 | |||
| 103 | 5 | public function setSystemEnvironment(?string $environment): ConfigurableProcessor |
|
| 104 | { |
||
| 105 | 5 | $this->systemEnvironment = $environment; |
|
| 106 | |||
| 107 | 5 | return $this; |
|
| 108 | } |
||
| 109 | |||
| 110 | 82 | public function hydrate(string $environment): void |
|
| 111 | { |
||
| 112 | 82 | $files = $this->collectFiles(); |
|
| 113 | |||
| 114 | 82 | foreach($files as $file) |
|
| 115 | { |
||
| 116 | 82 | $this->hydrateFile($file, $environment); |
|
| 117 | } |
||
| 118 | |||
| 119 | 60 | if($this->nonDistFilesOverwriteAllowed === true) |
|
| 120 | { |
||
| 121 | 3 | $this->copyNonDistFiles(); |
|
| 122 | } |
||
| 123 | |||
| 124 | 60 | $this->info(sprintf( |
|
| 125 | 60 | '%d files generated', |
|
| 126 | 60 | count($files) |
|
| 127 | )); |
||
| 128 | 60 | } |
|
| 129 | |||
| 130 | 86 | private function collectFiles(): iterable |
|
| 131 | { |
||
| 132 | 86 | $pattern = sprintf('.*%s$', preg_quote($this->suffix, '~')); |
|
| 133 | |||
| 134 | 86 | return $this->finder->findFiles(sprintf('~%s~', $pattern)); |
|
| 135 | } |
||
| 136 | |||
| 137 | 3 | private function copyNonDistFiles(): void |
|
| 138 | { |
||
| 139 | 3 | $filesToCopy = $this->collectNonDistFiles(); |
|
| 140 | |||
| 141 | 3 | foreach($filesToCopy as $file) |
|
| 142 | { |
||
| 143 | 2 | $this->target->write($file, $this->sources->read($file)); |
|
| 144 | } |
||
| 145 | 3 | } |
|
| 146 | |||
| 147 | 3 | private function collectNonDistFiles(): iterable |
|
| 148 | { |
||
| 149 | 3 | $pattern = sprintf('(?<!%s)$', preg_quote($this->suffix, '~')); |
|
| 150 | |||
| 151 | 3 | return $this->finder->findFiles(sprintf('~%s~', $pattern)); |
|
| 152 | } |
||
| 153 | |||
| 154 | |||
| 155 | 82 | private function hydrateFile(string $file, string $environment): void |
|
| 156 | { |
||
| 157 | 82 | $this->currentTargetFile = preg_replace(sprintf( |
|
| 158 | 82 | '~(.*)(%s)$~', |
|
| 159 | 82 | preg_quote($this->suffix, '~') |
|
| 160 | 82 | ), '$1', $file); |
|
| 161 | |||
| 162 | 82 | if($this->nonDistFilesOverwriteAllowed) |
|
| 163 | { |
||
| 164 | 4 | $this->currentTargetFile = (new \SplFileInfo($this->currentTargetFile))->getFilename(); |
|
| 165 | } |
||
| 166 | |||
| 167 | 82 | $content = (string) $this->sources->read($file); |
|
| 168 | 82 | $replacementCounter = $this->parseFileDirectives($file, $content, $environment); |
|
| 169 | |||
| 170 | 63 | $targetContent = $this->injectValues($file, $content, $environment, $replacementCounter); |
|
| 171 | |||
| 172 | 61 | $this->debug("Write $this->currentTargetFile"); |
|
| 173 | |||
| 174 | 61 | if($this->dryRun === false) |
|
| 175 | { |
||
| 176 | 60 | if($this->hasBeenHydrated($this->currentTargetFile) && $this->nonDistFilesOverwriteAllowed) |
|
| 177 | { |
||
| 178 | 1 | throw new \RuntimeException(sprintf('The fileName "%s" is defined in 2 config folders (not allowed with targetPath config enabled)', $this->currentTargetFile)); |
|
| 179 | } |
||
| 180 | |||
| 181 | 60 | $this->backupFile($this->currentTargetFile); |
|
| 182 | 60 | $this->target->write($this->currentTargetFile, $targetContent, true); |
|
| 183 | } |
||
| 184 | |||
| 185 | 61 | $this->hydratedFiles[$this->currentTargetFile] = $replacementCounter; |
|
| 186 | 61 | } |
|
| 187 | |||
| 188 | 60 | private function hasBeenHydrated(string $file): bool |
|
| 189 | { |
||
| 190 | 60 | return array_key_exists($file, $this->hydratedFiles); |
|
| 191 | } |
||
| 192 | |||
| 193 | 82 | private function parseFileDirectives(string $file, string & $fileContent, string $environment): int |
|
| 194 | { |
||
| 195 | 82 | $this->currentFormatterName = null; |
|
| 196 | |||
| 197 | 82 | $this->parseFormatterDirective($file, $fileContent); |
|
| 198 | 81 | $replacementCounter = $this->parseListDirective($file, $fileContent, $environment); |
|
| 199 | |||
| 200 | 63 | $fileContent = $this->removeFileDirectives($fileContent); |
|
| 201 | |||
| 202 | 63 | return $replacementCounter; |
|
| 203 | } |
||
| 204 | |||
| 205 | 82 | private function parseFormatterDirective(string $file, string $fileContent): void |
|
| 206 | { |
||
| 207 | 82 | if($count = preg_match_all('~<%\s*karma:formatter\s*=\s*(?P<formatterName>[^%]+)%>~', $fileContent, $matches)) |
|
| 208 | { |
||
| 209 | 3 | if($count !== 1) |
|
| 210 | { |
||
| 211 | 1 | throw new \RuntimeException(sprintf( |
|
| 212 | 1 | 'Syntax error in %s : only one formatter directive is allowed (%d found)', |
|
| 213 | $file, |
||
| 214 | $count |
||
| 215 | )); |
||
| 216 | } |
||
| 217 | |||
| 218 | 2 | $this->currentFormatterName = strtolower(trim($matches['formatterName'][0])); |
|
| 219 | } |
||
| 220 | 81 | } |
|
| 221 | |||
| 222 | 81 | private function parseListDirective(string $file, string & $fileContent, string $environment): int |
|
| 223 | { |
||
| 224 | 81 | $replacementCounter = 0; |
|
| 225 | |||
| 226 | 81 | $regexDelimiter = '(delimiter="(?P<delimiterName>[^"]*)")?'; |
|
| 227 | 81 | $regexWrapper = '(wrapper="(?P<wrapperPrefix>[^"]*)":"(?P<wrapperSuffix>[^"]*)")?'; |
|
| 228 | 81 | $regex = '~<%\s*karma:list\s*var=(?P<variableName>[\S]+)\s*' . $regexDelimiter . '\s*' . $regexWrapper . '\s*%>~i'; |
|
| 229 | |||
| 230 | 81 | while(preg_match($regex, $fileContent, $matches)) |
|
| 231 | { |
||
| 232 | 30 | $delimiter = ''; |
|
| 233 | 30 | if(isset($matches['delimiterName'])) |
|
| 234 | { |
||
| 235 | 26 | $delimiter = $matches['delimiterName']; |
|
| 236 | } |
||
| 237 | |||
| 238 | 30 | $wrapper = ['prefix' => '', 'suffix' => '']; |
|
| 239 | 30 | if(isset($matches['wrapperPrefix'], $matches['wrapperSuffix'])) |
|
| 240 | { |
||
| 241 | $wrapper = [ |
||
| 242 | 9 | 'prefix' => $matches['wrapperPrefix'], |
|
| 243 | 9 | 'suffix' => $matches['wrapperSuffix'] |
|
| 244 | ]; |
||
| 245 | } |
||
| 246 | |||
| 247 | 30 | $generatedList = $this->generateContentForListDirective($matches['variableName'], $environment, $delimiter, $wrapper); |
|
| 248 | 29 | $fileContent = str_replace($matches[0], $generatedList, $fileContent); |
|
| 249 | |||
| 250 | 29 | $replacementCounter++; |
|
| 251 | } |
||
| 252 | |||
| 253 | 80 | $this->lookingForSyntaxErrorInListDirective($file, $fileContent); |
|
| 254 | |||
| 255 | 63 | return $replacementCounter; |
|
| 256 | } |
||
| 257 | |||
| 258 | 80 | private function lookingForSyntaxErrorInListDirective(string $file, string $fileContent): void |
|
| 259 | { |
||
| 260 | 80 | if(preg_match('~<%.*karma\s*:\s*list\s*~i', $fileContent)) |
|
| 261 | { |
||
| 262 | // karma:list detected but has not matches full regexp |
||
| 263 | 17 | throw new \RuntimeException("Invalid karma:list directive in file $file"); |
|
| 264 | } |
||
| 265 | 63 | } |
|
| 266 | |||
| 267 | 30 | private function generateContentForListDirective(string $variable, string $environment, string $delimiter, array $wrapper): string |
|
| 268 | { |
||
| 269 | 30 | $values = $this->readValueToInject($variable, $environment); |
|
| 270 | 29 | $formatter = $this->getFormatterForCurrentTargetFile(); |
|
| 271 | |||
| 272 | 29 | if(! is_array($values)) |
|
| 273 | { |
||
| 274 | 8 | $values = [$values]; |
|
| 275 | } |
||
| 276 | |||
| 277 | 29 | array_walk($values, function (& $value) use ($formatter) { |
|
| 278 | 24 | $value = $formatter->format($value); |
|
| 279 | 29 | }); |
|
| 280 | |||
| 281 | 29 | $generated = implode($delimiter, $values); |
|
| 282 | 29 | return sprintf( |
|
| 283 | 29 | '%s%s%s', |
|
| 284 | 29 | ! empty($generated) ? $wrapper['prefix'] : '', |
|
| 285 | $generated, |
||
| 286 | 29 | ! empty($generated) ? $wrapper['suffix'] : '' |
|
| 287 | ); |
||
| 288 | } |
||
| 289 | |||
| 290 | 63 | private function removeFileDirectives($fileContent) |
|
| 291 | { |
||
| 292 | 63 | return preg_replace('~(<%\s*karma:[^%]*%>\s*)~i', '', $fileContent); |
|
| 293 | } |
||
| 294 | |||
| 295 | 63 | private function injectValues(string $sourceFile, string $content, string $environment, int & $replacementCounter = 0): string |
|
| 296 | { |
||
| 297 | 63 | $replacementCounter += $this->injectScalarValues($content, $environment); |
|
| 298 | 62 | $replacementCounter += $this->injectListValues($content, $environment); |
|
| 299 | |||
| 300 | 61 | if($replacementCounter === 0) |
|
| 301 | { |
||
| 302 | 9 | $this->warning("No variable found in $sourceFile"); |
|
| 303 | } |
||
| 304 | |||
| 305 | 61 | return $content; |
|
| 306 | } |
||
| 307 | |||
| 308 | 60 | private function readValueToInject(string $variableName, string $environment) |
|
| 309 | { |
||
| 310 | 60 | if($this->systemEnvironment !== null && $this->reader->isSystem($variableName) === true) |
|
| 311 | { |
||
| 312 | 4 | $environment = $this->systemEnvironment; |
|
| 313 | } |
||
| 314 | |||
| 315 | 60 | $this->markVariableAsUsed($variableName); |
|
| 316 | |||
| 317 | 60 | $value = $this->reader->read($variableName, $environment); |
|
| 318 | |||
| 319 | 59 | $this->checkValueIsAllowed($variableName, $environment, $value); |
|
| 320 | |||
| 321 | 58 | return $value; |
|
| 322 | } |
||
| 323 | |||
| 324 | 59 | private function checkValueIsAllowed(string $variableName, string $environment, $value): void |
|
| 325 | { |
||
| 326 | 59 | if($value === self::FIXME_VALUE) |
|
| 327 | { |
||
| 328 | 1 | throw new \RuntimeException(sprintf( |
|
| 329 | 1 | 'Missing value for variable %s in environment %s (FIXME marker found)', |
|
| 330 | $variableName, |
||
| 331 | $environment |
||
| 332 | )); |
||
| 333 | } |
||
| 334 | |||
| 335 | 58 | if($value === self::TODO_VALUE) |
|
| 336 | { |
||
| 337 | 2 | $this->unvaluedVariables[] = $variableName; |
|
| 338 | } |
||
| 339 | 58 | } |
|
| 340 | |||
| 341 | 63 | private function getFormatterForCurrentTargetFile(): Formatter |
|
| 342 | { |
||
| 343 | 63 | $fileExtension = pathinfo($this->currentTargetFile, PATHINFO_EXTENSION); |
|
| 344 | |||
| 345 | 63 | return $this->formatterProvider->getFormatter($fileExtension, $this->currentFormatterName); |
|
| 346 | } |
||
| 347 | |||
| 348 | 63 | private function injectScalarValues(string & $content, string $environment): int |
|
| 349 | { |
||
| 350 | 63 | $formatter = $this->getFormatterForCurrentTargetFile(); |
|
| 351 | |||
| 352 | 63 | $content = preg_replace_callback(self::VARIABLE_REGEX, function(array $matches) use($environment, $formatter) |
|
| 353 | { |
||
| 354 | 34 | $value = $this->readValueToInject($matches['variableName'], $environment); |
|
| 355 | |||
| 356 | 33 | if(is_array($value)) |
|
| 357 | { |
||
| 358 | // don't replace lists at this time |
||
| 359 | 14 | return $matches[0]; |
|
| 360 | } |
||
| 361 | |||
| 362 | 26 | return $formatter->format($value); |
|
| 363 | |||
| 364 | 63 | }, $content, -1, $count); |
|
| 365 | |||
| 366 | 62 | return $count; |
|
| 367 | } |
||
| 368 | |||
| 369 | 62 | private function injectListValues(string & $content, string $environment): int |
|
| 370 | { |
||
| 371 | 62 | $formatter = $this->getFormatterForCurrentTargetFile(); |
|
| 372 | 62 | $replacementCounter = 0; |
|
| 373 | |||
| 374 | 62 | $eol = $this->detectEol($content); |
|
| 375 | |||
| 376 | 62 | while(preg_match(self::VARIABLE_REGEX, $content)) |
|
| 377 | { |
||
| 378 | 15 | $lines = explode($eol, $content); |
|
| 379 | 15 | $result = []; |
|
| 380 | |||
| 381 | 15 | foreach($lines as $lineNumber => $line) |
|
| 382 | { |
||
| 383 | 15 | if(preg_match(self::VARIABLE_REGEX, $line, $matches)) |
|
| 384 | { |
||
| 385 | 15 | $values = $this->readValueToInject($matches['variableName'], $environment); |
|
| 386 | |||
| 387 | 15 | if(!is_array($values)) |
|
| 388 | { |
||
| 389 | 1 | throw new \RuntimeException(sprintf( |
|
| 390 | 1 | "Nested variable detected [%s] while writing %s at line %d", |
|
| 391 | 1 | $matches['variableName'], |
|
| 392 | 1 | $this->currentTargetFile, |
|
| 393 | $lineNumber |
||
| 394 | )); |
||
| 395 | } |
||
| 396 | |||
| 397 | 14 | $replacementCounter++; |
|
| 398 | 14 | foreach($values as $value) |
|
| 399 | { |
||
| 400 | 14 | $result[] = preg_replace(self::VARIABLE_REGEX, $formatter->format($value), $line, 1); |
|
| 401 | } |
||
| 402 | |||
| 403 | 14 | continue; |
|
| 404 | } |
||
| 405 | |||
| 406 | 8 | $result[] = $line; |
|
| 407 | } |
||
| 408 | |||
| 409 | 14 | $content = implode($eol, $result); |
|
| 410 | } |
||
| 411 | |||
| 412 | 61 | return $replacementCounter; |
|
| 413 | } |
||
| 414 | |||
| 415 | 62 | private function detectEol(string $content): string |
|
| 416 | { |
||
| 417 | 62 | $types = array("\r\n", "\r", "\n"); |
|
| 418 | |||
| 419 | 62 | foreach($types as $type) |
|
| 420 | { |
||
| 421 | 62 | if(strpos($content, $type) !== false) |
|
| 422 | { |
||
| 423 | 14 | return $type; |
|
| 424 | } |
||
| 425 | } |
||
| 426 | |||
| 427 | 53 | return "\n"; |
|
| 428 | } |
||
| 429 | |||
| 430 | 60 | private function backupFile(string $targetFile): void |
|
| 431 | { |
||
| 432 | 60 | if($this->enableBackup === true) |
|
| 433 | { |
||
| 434 | 1 | if($this->target->has($targetFile)) |
|
| 435 | { |
||
| 436 | 1 | $backupFile = $targetFile . Application::BACKUP_SUFFIX; |
|
| 437 | 1 | $this->target->write($backupFile, $this->target->read($targetFile), true); |
|
| 438 | } |
||
| 439 | } |
||
| 440 | 60 | } |
|
| 441 | |||
| 442 | 4 | public function rollback(): void |
|
| 443 | { |
||
| 444 | 4 | $files = $this->collectFiles(); |
|
| 445 | |||
| 446 | 4 | foreach($files as $file) |
|
| 447 | { |
||
| 448 | 2 | $this->rollbackFile($file); |
|
| 449 | } |
||
| 450 | 4 | } |
|
| 451 | |||
| 452 | 2 | private function rollbackFile(string $file): void |
|
| 453 | { |
||
| 454 | 2 | $this->debug("- $file"); |
|
| 455 | |||
| 456 | 2 | $targetFile = substr($file, 0, strlen($this->suffix) * -1); |
|
| 457 | 2 | $backupFile = $targetFile . Application::BACKUP_SUFFIX; |
|
| 458 | |||
| 459 | 2 | if($this->sources->has($backupFile)) |
|
| 460 | { |
||
| 461 | 2 | $this->info(" Writing $targetFile"); |
|
| 462 | |||
| 463 | 2 | if($this->dryRun === false) |
|
| 464 | { |
||
| 465 | 1 | $backupContent = $this->sources->read($backupFile); |
|
| 466 | 1 | $this->sources->write($targetFile, $backupContent, true); |
|
| 467 | } |
||
| 468 | } |
||
| 469 | 2 | } |
|
| 470 | |||
| 471 | 9 | public function getUnusedVariables(): array |
|
| 472 | { |
||
| 473 | 9 | return array_merge(array_flip($this->unusedVariables)); |
|
| 474 | } |
||
| 475 | |||
| 476 | 60 | private function markVariableAsUsed(string $variableName): void |
|
| 477 | { |
||
| 478 | 60 | if(isset($this->unusedVariables[$variableName])) |
|
| 479 | { |
||
| 480 | 58 | unset($this->unusedVariables[$variableName]); |
|
| 481 | } |
||
| 482 | 60 | } |
|
| 483 | |||
| 484 | 9 | public function getUnvaluedVariables(): array |
|
| 485 | { |
||
| 486 | 9 | return $this->unvaluedVariables; |
|
| 487 | } |
||
| 488 | |||
| 489 | public function hydratedFiles(): array |
||
| 490 | { |
||
| 491 | return $this->hydratedFiles; |
||
| 494 |