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.