Complex classes like AddPrefixCommand 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 AddPrefixCommand, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 35 | final class AddPrefixCommand extends BaseCommand |
||
| 36 | { |
||
| 37 | private const PATH_ARG = 'paths'; |
||
| 38 | private const PREFIX_OPT = 'prefix'; |
||
| 39 | private const OUTPUT_DIR_OPT = 'output-dir'; |
||
| 40 | private const FORCE_OPT = 'force'; |
||
| 41 | private const STOP_ON_FAILURE_OPT = 'stop-on-failure'; |
||
| 42 | private const CONFIG_FILE_OPT = 'config'; |
||
| 43 | private const CONFIG_FILE_DEFAULT = 'scoper.inc.php'; |
||
| 44 | private const NO_CONFIG_OPT = 'no-config'; |
||
| 45 | |||
| 46 | private $fileSystem; |
||
| 47 | private $scoper; |
||
| 48 | private $init = false; |
||
| 49 | |||
| 50 | /** |
||
| 51 | * @inheritdoc |
||
| 52 | */ |
||
| 53 | 16 | public function __construct(Filesystem $fileSystem, Scoper $scoper) |
|
| 54 | { |
||
| 55 | 16 | parent::__construct(); |
|
| 56 | |||
| 57 | 16 | $this->fileSystem = $fileSystem; |
|
| 58 | 16 | $this->scoper = $scoper; |
|
| 59 | } |
||
| 60 | |||
| 61 | /** |
||
| 62 | * @inheritdoc |
||
| 63 | */ |
||
| 64 | 16 | protected function configure(): void |
|
| 65 | { |
||
| 66 | 16 | parent::configure(); |
|
| 67 | |||
| 68 | $this |
||
| 69 | 16 | ->setName('add-prefix') |
|
| 70 | 16 | ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.') |
|
| 71 | 16 | ->addArgument( |
|
| 72 | 16 | self::PATH_ARG, |
|
| 73 | 16 | InputArgument::IS_ARRAY, |
|
| 74 | 16 | 'The path(s) to process.' |
|
| 75 | ) |
||
| 76 | 16 | ->addOption( |
|
| 77 | 16 | self::PREFIX_OPT, |
|
| 78 | 16 | 'p', |
|
| 79 | 16 | InputOption::VALUE_REQUIRED, |
|
| 80 | 16 | 'The namespace prefix to add.' |
|
| 81 | ) |
||
| 82 | 16 | ->addOption( |
|
| 83 | 16 | self::OUTPUT_DIR_OPT, |
|
| 84 | 16 | 'o', |
|
| 85 | 16 | InputOption::VALUE_REQUIRED, |
|
| 86 | 16 | 'The output directory in which the prefixed code will be dumped.', |
|
| 87 | 16 | 'build' |
|
| 88 | ) |
||
| 89 | 16 | ->addOption( |
|
| 90 | 16 | self::FORCE_OPT, |
|
| 91 | 16 | 'f', |
|
| 92 | 16 | InputOption::VALUE_NONE, |
|
| 93 | 16 | 'Deletes any existing content in the output directory without any warning.' |
|
| 94 | ) |
||
| 95 | 16 | ->addOption( |
|
| 96 | 16 | self::STOP_ON_FAILURE_OPT, |
|
| 97 | 16 | 's', |
|
| 98 | 16 | InputOption::VALUE_NONE, |
|
| 99 | 16 | 'Stops on failure.' |
|
| 100 | ) |
||
| 101 | 16 | ->addOption( |
|
| 102 | 16 | self::CONFIG_FILE_OPT, |
|
| 103 | 16 | 'c', |
|
| 104 | 16 | InputOption::VALUE_REQUIRED, |
|
| 105 | 16 | sprintf( |
|
| 106 | 16 | 'Configuration file. Will use "%s" if found by default.', |
|
| 107 | 16 | self::CONFIG_FILE_DEFAULT |
|
| 108 | ) |
||
| 109 | ) |
||
| 110 | 16 | ->addOption( |
|
| 111 | 16 | self::NO_CONFIG_OPT, |
|
| 112 | 16 | null, |
|
| 113 | 16 | InputOption::VALUE_NONE, |
|
| 114 | 16 | 'Do not look for a configuration file.' |
|
| 115 | ) |
||
| 116 | ; |
||
| 117 | } |
||
| 118 | |||
| 119 | /** |
||
| 120 | * @inheritdoc |
||
| 121 | */ |
||
| 122 | 14 | protected function execute(InputInterface $input, OutputInterface $output): int |
|
| 123 | { |
||
| 124 | 14 | $io = new SymfonyStyle($input, $output); |
|
| 125 | 14 | $io->writeln(''); |
|
| 126 | |||
| 127 | 14 | $this->changeWorkingDirectory($input); |
|
| 128 | |||
| 129 | 14 | $this->validatePrefix($input); |
|
| 130 | 14 | $this->validatePaths($input); |
|
| 131 | 14 | $this->validateOutputDir($input, $io); |
|
| 132 | |||
| 133 | 14 | $config = $this->retrieveConfig($input, $output, $io); |
|
| 134 | 12 | $output = $input->getOption(self::OUTPUT_DIR_OPT); |
|
| 135 | |||
| 136 | 12 | $logger = new ConsoleLogger( |
|
| 137 | 12 | $this->getApplication(), |
|
| 138 | 12 | $io |
|
| 139 | ); |
||
| 140 | |||
| 141 | 12 | $logger->outputScopingStart( |
|
| 142 | 12 | $config->getPrefix(), |
|
| 143 | 12 | $input->getArgument(self::PATH_ARG) |
|
| 144 | ); |
||
| 145 | |||
| 146 | try { |
||
| 147 | 12 | $this->scopeFiles( |
|
| 148 | 12 | $config->getPrefix(), |
|
| 149 | 12 | $config->getFilesWithContents(), |
|
| 150 | 12 | $output, |
|
| 151 | 12 | $config->getPatchers(), |
|
| 152 | 12 | $config->getWhitelist(), |
|
| 153 | 12 | $input->getOption(self::STOP_ON_FAILURE_OPT), |
|
| 154 | 12 | $logger |
|
| 155 | ); |
||
| 156 | } catch (Throwable $throwable) { |
||
| 157 | $this->fileSystem->remove($output); |
||
| 158 | |||
| 159 | $logger->outputScopingEndWithFailure(); |
||
| 160 | |||
| 161 | throw $throwable; |
||
| 162 | } |
||
| 163 | |||
| 164 | 12 | $logger->outputScopingEnd(); |
|
| 165 | |||
| 166 | 12 | return 0; |
|
| 167 | } |
||
| 168 | |||
| 169 | /** |
||
| 170 | * @var callable[] |
||
| 171 | */ |
||
| 172 | 12 | private function scopeFiles( |
|
| 225 | |||
| 226 | /** |
||
| 227 | * @param callable[] $patchers |
||
| 228 | */ |
||
| 229 | 12 | private function scopeFile( |
|
| 230 | string $inputFilePath, |
||
| 231 | string $inputContents, |
||
| 232 | string $outputFilePath, |
||
| 233 | string $prefix, |
||
| 234 | array $patchers, |
||
| 235 | Whitelist $whitelist, |
||
| 236 | bool $stopOnFailure, |
||
| 237 | ConsoleLogger $logger |
||
| 238 | ): void { |
||
| 239 | try { |
||
| 240 | 12 | $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist); |
|
| 241 | 2 | } catch (Throwable $throwable) { |
|
| 242 | 2 | $exception = new ParsingException( |
|
| 243 | 2 | sprintf( |
|
| 244 | 2 | 'Could not parse the file "%s".', |
|
| 245 | 2 | $inputFilePath |
|
| 246 | ), |
||
| 247 | 2 | 0, |
|
| 248 | 2 | $throwable |
|
| 249 | ); |
||
| 250 | |||
| 251 | 2 | if ($stopOnFailure) { |
|
| 252 | throw $exception; |
||
| 253 | } |
||
| 254 | |||
| 255 | 2 | $logger->outputWarnOfFailure($inputFilePath, $exception); |
|
| 256 | |||
| 257 | 2 | $scoppedContent = file_get_contents($inputFilePath); |
|
| 258 | } |
||
| 259 | |||
| 260 | 12 | $this->fileSystem->dumpFile($outputFilePath, $scoppedContent); |
|
| 261 | |||
| 262 | 12 | if (false === isset($exception)) { |
|
| 263 | 11 | $logger->outputSuccess($inputFilePath); |
|
| 264 | } |
||
| 265 | } |
||
| 266 | |||
| 267 | 14 | private function validatePrefix(InputInterface $input): void |
|
| 277 | |||
| 278 | 14 | private function validatePaths(InputInterface $input): void |
|
| 279 | { |
||
| 280 | 14 | $cwd = getcwd(); |
|
| 281 | 14 | $fileSystem = $this->fileSystem; |
|
| 296 | |||
| 297 | 14 | private function validateOutputDir(InputInterface $input, OutputStyle $io): void |
|
| 358 | |||
| 359 | 14 | private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration |
|
| 450 | |||
| 451 | 12 | private function retrievePaths(InputInterface $input, Configuration $config): Configuration |
|
| 462 | |||
| 463 | 4 | private function makeAbsolutePath(string $path): string |
|
| 471 | |||
| 472 | 1 | private function generateRandomPrefix(): string |
|
| 476 | } |
||
| 477 |
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.