Total Complexity | 53 |
Total Lines | 599 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Compile 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.
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 Compile, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
63 | class Compile extends Configurable |
||
64 | { |
||
65 | use ChangeableWorkingDirectory; |
||
66 | |||
67 | private const HELP = <<<'HELP' |
||
68 | The <info>%command.name%</info> command will compile code in a new PHAR based on a variety of settings. |
||
69 | <comment> |
||
70 | This command relies on a configuration file for loading |
||
71 | PHAR packaging settings. If a configuration file is not |
||
72 | specified through the <info>--config|-c</info> option, one of |
||
73 | the following files will be used (in order): <info>box.json</info>, |
||
74 | <info>box.json.dist</info> |
||
75 | </comment> |
||
76 | The configuration file is actually a JSON object saved to a file. For more |
||
77 | information check the documentation online: |
||
78 | <comment> |
||
79 | https://github.com/humbug/box |
||
80 | </comment> |
||
81 | HELP; |
||
82 | |||
83 | private const DEBUG_OPTION = 'debug'; |
||
84 | private const DEV_OPTION = 'dev'; |
||
85 | private const NO_CONFIG_OPTION = 'no-config'; |
||
86 | |||
87 | /** |
||
88 | * {@inheritdoc} |
||
89 | */ |
||
90 | protected function configure(): void |
||
91 | { |
||
92 | parent::configure(); |
||
93 | |||
94 | $this->setName('compile'); |
||
95 | $this->setDescription('Compile an application into a PHAR'); |
||
96 | $this->setHelp(self::HELP); |
||
97 | |||
98 | $this->addOption( |
||
99 | self::DEBUG_OPTION, |
||
100 | null, |
||
101 | InputOption::VALUE_NONE, |
||
102 | 'Dump the files added to the PHAR in a `'.Box::DEBUG_DIR.'` directory' |
||
103 | ); |
||
104 | $this->addOption( |
||
105 | self::DEV_OPTION, |
||
106 | null, |
||
107 | InputOption::VALUE_NONE, |
||
108 | 'Skips the compression step' |
||
109 | ); |
||
110 | $this->addOption( |
||
111 | self::NO_CONFIG_OPTION, |
||
112 | null, |
||
113 | InputOption::VALUE_NONE, |
||
114 | 'Ignore the config file even when one is specified with the --config option' |
||
115 | ); |
||
116 | |||
117 | $this->configureWorkingDirOption(); |
||
118 | } |
||
119 | |||
120 | /** |
||
121 | * {@inheritdoc} |
||
122 | */ |
||
123 | protected function execute(InputInterface $input, OutputInterface $output): void |
||
124 | { |
||
125 | (new PhpSettingsHandler(new ConsoleLogger($output)))->check(); |
||
126 | |||
127 | if (true === $input->getOption(self::DEBUG_OPTION)) { |
||
128 | enable_debug($output); |
||
129 | } |
||
130 | |||
131 | $this->changeWorkingDirectory($input); |
||
132 | |||
133 | $io = new SymfonyStyle($input, $output); |
||
134 | |||
135 | $io->writeln($this->getApplication()->getHelp()); |
||
136 | $io->writeln(''); |
||
137 | |||
138 | $config = $input->getOption(self::NO_CONFIG_OPTION) |
||
139 | ? Configuration::create(null, new stdClass()) |
||
140 | : $this->getConfig($input, $output, true) |
||
141 | ; |
||
142 | $path = $config->getOutputPath(); |
||
143 | |||
144 | $logger = new BuildLogger($io); |
||
145 | |||
146 | $startTime = microtime(true); |
||
147 | |||
148 | $this->removeExistingArtefacts($config, $logger); |
||
149 | |||
150 | $logger->logStartBuilding($path); |
||
151 | |||
152 | $this->createPhar($config, $input, $output, $logger, $io); |
||
153 | |||
154 | $this->correctPermissions($path, $config, $logger); |
||
155 | |||
156 | $logger->log( |
||
157 | BuildLogger::STAR_PREFIX, |
||
158 | 'Done.' |
||
159 | ); |
||
160 | |||
161 | $io->comment( |
||
162 | sprintf( |
||
163 | "<info>PHAR size: %s\nMemory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>", |
||
164 | formatted_filesize($path), |
||
165 | round(memory_get_usage() / 1024 / 1024, 2), |
||
166 | round(memory_get_peak_usage() / 1024 / 1024, 2), |
||
167 | round(microtime(true) - $startTime, 2) |
||
168 | ) |
||
169 | ); |
||
170 | } |
||
171 | |||
172 | private function createPhar( |
||
173 | Configuration $config, |
||
174 | InputInterface $input, |
||
175 | OutputInterface $output, |
||
176 | BuildLogger $logger, |
||
177 | SymfonyStyle $io |
||
178 | ): void { |
||
179 | $box = Box::create( |
||
180 | $config->getTmpOutputPath() |
||
181 | ); |
||
182 | $box->getPhar()->startBuffering(); |
||
183 | |||
184 | $this->setReplacementValues($config, $box, $logger); |
||
185 | $this->registerCompactors($config, $box, $logger); |
||
186 | $this->registerFileMapping($config, $box, $logger); |
||
187 | |||
188 | // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary |
||
189 | // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise. |
||
190 | $main = $this->registerMainScript($config, $box, $logger); |
||
191 | |||
192 | $check = $this->registerRequirementsChecker($config, $box, $logger); |
||
193 | |||
194 | $this->addFiles($config, $box, $logger, $io); |
||
195 | |||
196 | $this->registerStub($config, $box, $main, $check, $logger); |
||
197 | $this->configureMetadata($config, $box, $logger); |
||
198 | |||
199 | $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $logger); |
||
200 | |||
201 | $box->getPhar()->stopBuffering(); |
||
202 | |||
203 | $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger); |
||
204 | |||
205 | if ($config->getTmpOutputPath() !== $config->getOutputPath()) { |
||
206 | rename($config->getTmpOutputPath(), $config->getOutputPath()); |
||
207 | } |
||
208 | } |
||
209 | |||
210 | private function removeExistingArtefacts(Configuration $config, BuildLogger $logger): void |
||
211 | { |
||
212 | $path = $config->getOutputPath(); |
||
213 | |||
214 | if (is_debug_enabled()) { |
||
215 | $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM); |
||
216 | $file = null !== $config->getFile() ? $config->getFile() : 'No config file'; |
||
217 | |||
218 | remove(Box::DEBUG_DIR); |
||
219 | |||
220 | dump_file( |
||
221 | Box::DEBUG_DIR.'/.box_configuration', |
||
222 | <<<EOF |
||
223 | // |
||
224 | // Processed content of the configuration file "$file" dumped for debugging purposes |
||
225 | // Time: $date |
||
226 | // |
||
227 | |||
228 | |||
229 | EOF |
||
230 | .(new CliDumper())->dump( |
||
231 | (new VarCloner())->cloneVar($config), |
||
232 | true |
||
233 | ) |
||
234 | ); |
||
235 | } |
||
236 | |||
237 | if (false === file_exists($path)) { |
||
238 | return; |
||
239 | } |
||
240 | |||
241 | $logger->log( |
||
242 | BuildLogger::QUESTION_MARK_PREFIX, |
||
243 | sprintf( |
||
244 | 'Removing the existing PHAR "%s"', |
||
245 | $path |
||
246 | ) |
||
247 | ); |
||
248 | |||
249 | remove($path); |
||
250 | } |
||
251 | |||
252 | private function setReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void |
||
253 | { |
||
254 | $values = $config->getProcessedReplacements(); |
||
255 | |||
256 | if ([] === $values) { |
||
257 | return; |
||
258 | } |
||
259 | |||
260 | $logger->log( |
||
261 | BuildLogger::QUESTION_MARK_PREFIX, |
||
262 | 'Setting replacement values' |
||
263 | ); |
||
264 | |||
265 | foreach ($values as $key => $value) { |
||
266 | $logger->log( |
||
267 | BuildLogger::PLUS_PREFIX, |
||
268 | sprintf( |
||
269 | '%s: %s', |
||
270 | $key, |
||
271 | $value |
||
272 | ) |
||
273 | ); |
||
274 | } |
||
275 | |||
276 | $box->registerPlaceholders($values); |
||
277 | } |
||
278 | |||
279 | private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void |
||
280 | { |
||
281 | $compactors = $config->getCompactors(); |
||
282 | |||
283 | if ([] === $compactors) { |
||
284 | $logger->log( |
||
285 | BuildLogger::QUESTION_MARK_PREFIX, |
||
286 | 'No compactor to register' |
||
287 | ); |
||
288 | |||
289 | return; |
||
290 | } |
||
291 | |||
292 | $logger->log( |
||
293 | BuildLogger::QUESTION_MARK_PREFIX, |
||
294 | 'Registering compactors' |
||
295 | ); |
||
296 | |||
297 | $logCompactors = function (Compactor $compactor) use ($logger): void { |
||
298 | $logger->log( |
||
299 | BuildLogger::PLUS_PREFIX, |
||
300 | get_class($compactor) |
||
301 | ); |
||
302 | }; |
||
303 | |||
304 | array_map($logCompactors, $compactors); |
||
305 | |||
306 | $box->registerCompactors($compactors); |
||
307 | } |
||
308 | |||
309 | private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void |
||
310 | { |
||
311 | $fileMapper = $config->getFileMapper(); |
||
312 | |||
313 | $this->logMap($fileMapper, $logger); |
||
314 | |||
315 | $box->registerFileMapping( |
||
316 | $config->getBasePath(), |
||
317 | $fileMapper |
||
318 | ); |
||
319 | } |
||
320 | |||
321 | private function addFiles(Configuration $config, Box $box, BuildLogger $logger, SymfonyStyle $io): void |
||
322 | { |
||
323 | $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files'); |
||
324 | |||
325 | $count = count($config->getBinaryFiles()); |
||
326 | |||
327 | $box->addFiles($config->getBinaryFiles(), true); |
||
328 | |||
329 | $logger->log( |
||
330 | BuildLogger::CHEVRON_PREFIX, |
||
331 | 0 === $count |
||
332 | ? 'No file found' |
||
333 | : sprintf('%d file(s)', $count) |
||
334 | ); |
||
335 | |||
336 | $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files'); |
||
337 | |||
338 | $count = count($config->getFiles()); |
||
339 | |||
340 | try { |
||
341 | $box->addFiles($config->getFiles(), false, null !== $config->getComposerJson()); |
||
342 | } catch (MultiReasonException $exception) { |
||
343 | // This exception is handled a different way to give me meaningful feedback to the user |
||
344 | foreach ($exception->getReasons() as $reason) { |
||
345 | $io->error($reason); |
||
346 | } |
||
347 | |||
348 | throw $exception; |
||
349 | } |
||
350 | |||
351 | $logger->log( |
||
352 | BuildLogger::CHEVRON_PREFIX, |
||
353 | 0 === $count |
||
354 | ? 'No file found' |
||
355 | : sprintf('%d file(s)', $count) |
||
356 | ); |
||
357 | } |
||
358 | |||
359 | private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string |
||
386 | } |
||
387 | |||
388 | private function registerRequirementsChecker(Configuration $config, Box $box, BuildLogger $logger): bool |
||
389 | { |
||
390 | if (false === $config->checkRequirements()) { |
||
391 | return false; |
||
392 | } |
||
393 | |||
394 | $logger->log( |
||
395 | BuildLogger::QUESTION_MARK_PREFIX, |
||
396 | 'Adding requirements checker' |
||
397 | ); |
||
398 | |||
399 | $checkFiles = RequirementsDumper::dump($config->getComposerLockDecodedContents()); |
||
400 | |||
401 | $scoper = $this->createScoper(); |
||
|
|||
402 | |||
403 | foreach ($checkFiles as $fileWithContents) { |
||
404 | [$file, $contents] = $fileWithContents; |
||
405 | |||
406 | // $contents = $scoper->scope($file, $contents); |
||
407 | |||
408 | $box->addFile('.box/'.$file, $contents, true); |
||
409 | } |
||
410 | |||
411 | return true; |
||
412 | } |
||
413 | |||
414 | private function registerStub(Configuration $config, Box $box, string $main, bool $checkRequirements, BuildLogger $logger): void |
||
415 | { |
||
416 | if ($config->isStubGenerated()) { |
||
417 | $logger->log( |
||
418 | BuildLogger::QUESTION_MARK_PREFIX, |
||
419 | 'Generating new stub' |
||
420 | ); |
||
421 | |||
422 | $stub = $this->createStub($config, $main, $checkRequirements, $logger); |
||
423 | |||
424 | $box->getPhar()->setStub($stub->generate()); |
||
425 | } elseif (null !== ($stub = $config->getStubPath())) { |
||
426 | $logger->log( |
||
427 | BuildLogger::QUESTION_MARK_PREFIX, |
||
428 | sprintf( |
||
429 | 'Using stub file: %s', |
||
430 | $stub |
||
431 | ) |
||
432 | ); |
||
433 | |||
434 | $box->registerStub($stub); |
||
435 | } else { |
||
436 | // TODO: add warning that the check requirements could not be added |
||
437 | $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias()); |
||
438 | |||
439 | Assertion::true( |
||
440 | $aliasWasAdded, |
||
441 | sprintf( |
||
442 | 'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.', |
||
443 | $config->getAlias() |
||
444 | ) |
||
445 | ); |
||
446 | |||
447 | $box->getPhar()->setDefaultStub($main); |
||
448 | |||
449 | $logger->log( |
||
450 | BuildLogger::QUESTION_MARK_PREFIX, |
||
451 | 'Using default stub' |
||
452 | ); |
||
453 | } |
||
454 | } |
||
455 | |||
456 | private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void |
||
457 | { |
||
458 | if (null !== ($metadata = $config->getMetadata())) { |
||
459 | $logger->log( |
||
460 | BuildLogger::QUESTION_MARK_PREFIX, |
||
461 | 'Setting metadata' |
||
462 | ); |
||
463 | |||
464 | $logger->log( |
||
465 | BuildLogger::MINUS_PREFIX, |
||
466 | is_string($metadata) ? $metadata : var_export($metadata, true) |
||
467 | ); |
||
468 | |||
469 | $box->getPhar()->setMetadata($metadata); |
||
470 | } |
||
471 | } |
||
472 | |||
473 | private function configureCompressionAlgorithm(Configuration $config, Box $box, bool $dev, BuildLogger $logger): void |
||
474 | { |
||
475 | if (null !== ($algorithm = $config->getCompressionAlgorithm())) { |
||
476 | $logger->log( |
||
477 | BuildLogger::QUESTION_MARK_PREFIX, |
||
478 | sprintf( |
||
479 | 'Compressing with the algorithm "<comment>%s</comment>"', |
||
480 | array_search($algorithm, get_phar_compression_algorithms(), true) |
||
1 ignored issue
–
show
|
|||
481 | ) |
||
482 | ); |
||
483 | |||
484 | $box->getPhar()->compressFiles($algorithm); |
||
485 | } else { |
||
486 | $logger->log( |
||
487 | BuildLogger::QUESTION_MARK_PREFIX, |
||
488 | $dev |
||
489 | ? 'No compression' |
||
490 | : '<error>No compression</error>' |
||
491 | ); |
||
492 | } |
||
493 | } |
||
494 | |||
495 | private function signPhar( |
||
496 | Configuration $config, |
||
497 | Box $box, |
||
498 | string $path, |
||
499 | InputInterface $input, |
||
500 | OutputInterface $output, |
||
501 | BuildLogger $logger |
||
502 | ): void { |
||
503 | // sign using private key, if applicable |
||
504 | //TODO: check that out |
||
505 | remove($path.'.pubkey'); |
||
506 | |||
507 | $key = $config->getPrivateKeyPath(); |
||
508 | |||
509 | if (null === $key) { |
||
510 | if (null !== ($algorithm = $config->getSigningAlgorithm())) { |
||
511 | $box->getPhar()->setSignatureAlgorithm($algorithm); |
||
512 | } |
||
513 | |||
514 | return; |
||
515 | } |
||
516 | |||
517 | $logger->log( |
||
518 | BuildLogger::QUESTION_MARK_PREFIX, |
||
519 | 'Signing using a private key' |
||
520 | ); |
||
521 | |||
522 | $passphrase = $config->getPrivateKeyPassphrase(); |
||
523 | |||
524 | if ($config->isPrivateKeyPrompt()) { |
||
525 | if (false === $input->isInteractive()) { |
||
526 | throw new RuntimeException( |
||
527 | sprintf( |
||
528 | 'Accessing to the private key "%s" requires a passphrase but none provided. Either ' |
||
529 | .'provide one or run this command in interactive mode.', |
||
530 | $key |
||
531 | ) |
||
532 | ); |
||
533 | } |
||
534 | |||
535 | /** @var $dialog QuestionHelper */ |
||
536 | $dialog = $this->getHelper('question'); |
||
537 | |||
538 | $question = new Question('Private key passphrase:'); |
||
539 | $question->setHidden(false); |
||
540 | $question->setHiddenFallback(false); |
||
541 | |||
542 | $passphrase = $dialog->ask($input, $output, $question); |
||
543 | |||
544 | $output->writeln(''); |
||
545 | } |
||
546 | |||
547 | $box->signUsingFile($key, $passphrase); |
||
548 | } |
||
549 | |||
550 | private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void |
||
551 | { |
||
552 | if (null !== ($chmod = $config->getFileMode())) { |
||
553 | $logger->log( |
||
554 | BuildLogger::QUESTION_MARK_PREFIX, |
||
555 | "Setting file permissions to <comment>$chmod</comment>" |
||
556 | ); |
||
557 | |||
558 | chmod($path, $chmod); |
||
559 | } |
||
560 | } |
||
561 | |||
562 | private function createStub(Configuration $config, ?string $main, bool $checkRequirements, BuildLogger $logger): StubGenerator |
||
617 | } |
||
618 | |||
619 | private function logMap(MapFile $fileMapper, BuildLogger $logger): void |
||
620 | { |
||
621 | $map = $fileMapper->getMap(); |
||
622 | |||
623 | if ([] === $map) { |
||
624 | return; |
||
625 | } |
||
626 | |||
627 | $logger->log( |
||
628 | BuildLogger::QUESTION_MARK_PREFIX, |
||
629 | 'Mapping paths' |
||
630 | ); |
||
631 | |||
632 | foreach ($map as $item) { |
||
633 | foreach ($item as $match => $replace) { |
||
634 | if ('' === $match) { |
||
635 | $match = '(all)'; |
||
636 | $replace .= '/'; |
||
637 | } |
||
638 | |||
639 | $logger->log( |
||
640 | BuildLogger::MINUS_PREFIX, |
||
641 | sprintf( |
||
642 | '%s <info>></info> %s', |
||
643 | $match, |
||
644 | $replace |
||
645 | ) |
||
646 | ); |
||
647 | } |
||
648 | } |
||
649 | } |
||
650 | |||
651 | private function createScoper(): SimpleScoper |
||
662 | ); |
||
663 | } |
||
664 | } |
||
665 |