Passed
Pull Request — master (#116)
by Théo
02:03
created

Compile::execute()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 54
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 54
rs 8.7449
c 0
b 0
f 0
cc 5
eloc 32
nc 16
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\Console\Command;
16
17
use Amp\MultiReasonException;
18
use Assert\Assertion;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Humbug\PhpScoper\PhpParser\TraverserFactory;
22
use Humbug\PhpScoper\Scoper\NullScoper;
23
use Humbug\PhpScoper\Scoper\PhpScoper;
24
use KevinGH\Box\Box;
25
use KevinGH\Box\Compactor;
26
use KevinGH\Box\Configuration;
27
use KevinGH\Box\Console\Logger\BuildLogger;
28
use KevinGH\Box\MapFile;
29
use KevinGH\Box\PhpScoper\SimpleScoper;
30
use KevinGH\Box\PhpSettingsHandler;
31
use KevinGH\Box\RequirementChecker\RequirementsDumper;
32
use KevinGH\Box\StubGenerator;
33
use PhpParser\ParserFactory;
34
use RuntimeException;
35
use stdClass;
36
use Symfony\Component\Console\Helper\QuestionHelper;
37
use Symfony\Component\Console\Input\InputInterface;
38
use Symfony\Component\Console\Input\InputOption;
39
use Symfony\Component\Console\Logger\ConsoleLogger;
40
use Symfony\Component\Console\Output\OutputInterface;
41
use Symfony\Component\Console\Question\Question;
42
use Symfony\Component\Console\Style\SymfonyStyle;
43
use Symfony\Component\VarDumper\Cloner\VarCloner;
44
use Symfony\Component\VarDumper\Dumper\CliDumper;
45
use const DATE_ATOM;
46
use function count;
47
use function Humbug\PhpScoper\create_reflector;
48
use function KevinGH\Box\disable_parallel_processing;
49
use function KevinGH\Box\FileSystem\chmod;
50
use function KevinGH\Box\FileSystem\dump_file;
51
use function KevinGH\Box\FileSystem\make_path_relative;
52
use function KevinGH\Box\FileSystem\remove;
53
use function KevinGH\Box\FileSystem\rename;
54
use function KevinGH\Box\formatted_filesize;
55
use function KevinGH\Box\get_phar_compression_algorithms;
56
use function putenv;
57
58
/**
59
 * @final
60
 * @private
61
 * TODO: make final when Build is removed
62
 */
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 NO_PARALLEL_PROCESSING_OPTION = 'no-parallel';
85
    private const NO_RESTART_OPTION = 'no-restart';
86
    private const DEV_OPTION = 'dev';
87
    private const NO_CONFIG_OPTION = 'no-config';
88
89
    private const DEBUG_DIR = '.box_dump';
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    protected function configure(): void
95
    {
96
        parent::configure();
97
98
        $this->setName('compile');
99
        $this->setDescription('Compile an application into a PHAR');
100
        $this->setHelp(self::HELP);
101
102
        $this->addOption(
103
            self::DEBUG_OPTION,
104
            null,
105
            InputOption::VALUE_NONE,
106
            'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory'
107
        );
108
        $this->addOption(
109
            self::NO_PARALLEL_PROCESSING_OPTION,
110
            null,
111
            InputOption::VALUE_NONE,
112
            'Disable the parallel processing'
113
        );
114
        $this->addOption(
115
            self::NO_RESTART_OPTION,
116
            null,
117
            InputOption::VALUE_NONE,
118
            'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`'
119
        );
120
        $this->addOption(
121
            self::DEV_OPTION,
122
            null,
123
            InputOption::VALUE_NONE,
124
            'Skips the compression step'
125
        );
126
        $this->addOption(
127
            self::NO_CONFIG_OPTION,
128
            null,
129
            InputOption::VALUE_NONE,
130
            'Ignore the config file even when one is specified with the --config option'
131
        );
132
133
        $this->configureWorkingDirOption();
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    protected function execute(InputInterface $input, OutputInterface $output): void
140
    {
141
        $io = new SymfonyStyle($input, $output);
142
143
        if ($input->getOption(self::NO_RESTART_OPTION)) {
144
            putenv('BOX_ALLOW_XDEBUG=1');
145
        }
146
147
        if ($debug = $input->getOption(self::DEBUG_OPTION)) {
148
            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
149
        }
150
151
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
152
153
        if ($input->getOption(self::NO_PARALLEL_PROCESSING_OPTION)) {
154
            disable_parallel_processing();
155
            $io->writeln('<info>[debug] Disabled parallel processing</info>', OutputInterface::VERBOSITY_DEBUG);
156
        }
157
158
        $this->changeWorkingDirectory($input);
159
160
        $io->writeln($this->getApplication()->getHelp());
161
        $io->writeln('');
162
163
        $config = $input->getOption(self::NO_CONFIG_OPTION)
164
            ? Configuration::create(null, new stdClass())
165
            : $this->getConfig($input, $output, true)
166
        ;
167
        $path = $config->getOutputPath();
168
169
        $logger = new BuildLogger($io);
170
171
        $startTime = microtime(true);
172
173
        $this->removeExistingArtefacts($config, $logger, $debug);
174
175
        $logger->logStartBuilding($path);
176
177
        $this->createPhar($config, $input, $output, $logger, $io, $debug);
178
179
        $this->correctPermissions($path, $config, $logger);
180
181
        $logger->log(
182
            BuildLogger::STAR_PREFIX,
183
            'Done.'
184
        );
185
186
        $io->comment(
187
            sprintf(
188
                "<info>PHAR size: %s\nMemory usage: %.2fMB (peak: %.2fMB), time: %.2fs<info>",
189
                formatted_filesize($path),
190
                round(memory_get_usage() / 1024 / 1024, 2),
191
                round(memory_get_peak_usage() / 1024 / 1024, 2),
192
                round(microtime(true) - $startTime, 2)
193
            )
194
        );
195
    }
196
197
    private function createPhar(
198
        Configuration $config,
199
        InputInterface $input,
200
        OutputInterface $output,
201
        BuildLogger $logger,
202
        SymfonyStyle $io,
203
        bool $debug
204
    ): void {
205
        $box = Box::create(
206
            $config->getTmpOutputPath()
207
        );
208
        $box->getPhar()->startBuffering();
209
210
        $this->setReplacementValues($config, $box, $logger);
211
        $this->registerCompactors($config, $box, $logger);
212
        $this->registerFileMapping($config, $box, $logger);
213
214
        // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary
215
        // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise.
216
        $main = $this->registerMainScript($config, $box, $logger);
217
218
        $check = $this->registerRequirementsChecker($config, $box, $logger);
219
220
        $this->addFiles($config, $box, $logger, $io);
221
222
        $this->registerStub($config, $box, $main, $check, $logger);
223
        $this->configureMetadata($config, $box, $logger);
224
225
        $this->configureCompressionAlgorithm($config, $box, $input->getOption(self::DEV_OPTION), $logger);
226
227
        $box->getPhar()->stopBuffering();
228
229
        if ($debug) {
230
            $box->getPhar()->extractTo(self::DEBUG_DIR, null, true);
231
        }
232
233
        $this->signPhar($config, $box, $config->getTmpOutputPath(), $input, $output, $logger);
234
235
        if ($config->getTmpOutputPath() !== $config->getOutputPath()) {
236
            rename($config->getTmpOutputPath(), $config->getOutputPath());
237
        }
238
    }
239
240
    private function removeExistingArtefacts(Configuration $config, BuildLogger $logger, bool $debug): void
241
    {
242
        $path = $config->getOutputPath();
243
244
        if ($debug) {
245
            remove(self::DEBUG_DIR);
246
247
            $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM);
248
            $file = null !== $config->getFile() ? $config->getFile() : 'No config file';
249
250
            remove(self::DEBUG_DIR);
251
252
            dump_file(
253
                self::DEBUG_DIR.'/.box_configuration',
254
                <<<EOF
255
//
256
// Processed content of the configuration file "$file" dumped for debugging purposes
257
// Time: $date
258
//
259
260
261
EOF
262
                .(new CliDumper())->dump(
263
                    (new VarCloner())->cloneVar($config),
264
                    true
265
                )
266
            );
267
        }
268
269
        if (false === file_exists($path)) {
270
            return;
271
        }
272
273
        $logger->log(
274
            BuildLogger::QUESTION_MARK_PREFIX,
275
            sprintf(
276
                'Removing the existing PHAR "%s"',
277
                $path
278
            )
279
        );
280
281
        remove($path);
282
    }
283
284
    private function setReplacementValues(Configuration $config, Box $box, BuildLogger $logger): void
285
    {
286
        $values = $config->getProcessedReplacements();
287
288
        if ([] === $values) {
289
            return;
290
        }
291
292
        $logger->log(
293
            BuildLogger::QUESTION_MARK_PREFIX,
294
            'Setting replacement values'
295
        );
296
297
        foreach ($values as $key => $value) {
298
            $logger->log(
299
                BuildLogger::PLUS_PREFIX,
300
                sprintf(
301
                    '%s: %s',
302
                    $key,
303
                    $value
304
                )
305
            );
306
        }
307
308
        $box->registerPlaceholders($values);
309
    }
310
311
    private function registerCompactors(Configuration $config, Box $box, BuildLogger $logger): void
312
    {
313
        $compactors = $config->getCompactors();
314
315
        if ([] === $compactors) {
316
            $logger->log(
317
                BuildLogger::QUESTION_MARK_PREFIX,
318
                'No compactor to register'
319
            );
320
321
            return;
322
        }
323
324
        $logger->log(
325
            BuildLogger::QUESTION_MARK_PREFIX,
326
            'Registering compactors'
327
        );
328
329
        $logCompactors = function (Compactor $compactor) use ($logger): void {
330
            $logger->log(
331
                BuildLogger::PLUS_PREFIX,
332
                get_class($compactor)
333
            );
334
        };
335
336
        array_map($logCompactors, $compactors);
337
338
        $box->registerCompactors($compactors);
339
    }
340
341
    private function registerFileMapping(Configuration $config, Box $box, BuildLogger $logger): void
342
    {
343
        $fileMapper = $config->getFileMapper();
344
345
        $this->logMap($fileMapper, $logger);
346
347
        $box->registerFileMapping(
348
            $config->getBasePath(),
349
            $fileMapper
350
        );
351
    }
352
353
    private function addFiles(Configuration $config, Box $box, BuildLogger $logger, SymfonyStyle $io): void
354
    {
355
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding binary files');
356
357
        $count = count($config->getBinaryFiles());
358
359
        $box->addFiles($config->getBinaryFiles(), true);
360
361
        $logger->log(
362
            BuildLogger::CHEVRON_PREFIX,
363
            0 === $count
364
                ? 'No file found'
365
                : sprintf('%d file(s)', $count)
366
        );
367
368
        $logger->log(BuildLogger::QUESTION_MARK_PREFIX, 'Adding files');
369
370
        $count = count($config->getFiles());
371
372
        try {
373
            $box->addFiles($config->getFiles(), false, null !== $config->getComposerJson());
374
        } catch (MultiReasonException $exception) {
375
            // This exception is handled a different way to give me meaningful feedback to the user
376
            foreach ($exception->getReasons() as $reason) {
377
                $io->error($reason);
378
            }
379
380
            throw $exception;
381
        }
382
383
        $logger->log(
384
            BuildLogger::CHEVRON_PREFIX,
385
            0 === $count
386
                ? 'No file found'
387
                : sprintf('%d file(s)', $count)
388
        );
389
    }
390
391
    private function registerMainScript(Configuration $config, Box $box, BuildLogger $logger): ?string
392
    {
393
        $main = $config->getMainScriptPath();
394
395
        $logger->log(
396
            BuildLogger::QUESTION_MARK_PREFIX,
397
            sprintf(
398
                'Adding main file: %s',
399
                $main
400
            )
401
        );
402
403
        $localMain = $box->addFile(
404
            $main,
405
            $config->getMainScriptContents()
406
        );
407
408
        $relativeMain = make_path_relative($main, $config->getBasePath());
409
410
        if ($localMain !== $relativeMain) {
411
            $logger->log(
412
                BuildLogger::CHEVRON_PREFIX,
413
                $localMain
414
            );
415
        }
416
417
        return $localMain;
418
    }
419
420
    private function registerRequirementsChecker(Configuration $config, Box $box, BuildLogger $logger): bool
421
    {
422
        if (false === $config->checkRequirements()) {
423
            return false;
424
        }
425
426
        $logger->log(
427
            BuildLogger::QUESTION_MARK_PREFIX,
428
            'Adding requirements checker'
429
        );
430
431
        $checkFiles = RequirementsDumper::dump($config->getComposerLockDecodedContents());
432
433
        foreach ($checkFiles as $fileWithContents) {
434
            [$file, $contents] = $fileWithContents;
435
436
            $box->addFile('.box/'.$file, $contents, true);
437
        }
438
439
        return true;
440
    }
441
442
    private function registerStub(Configuration $config, Box $box, string $main, bool $checkRequirements, BuildLogger $logger): void
443
    {
444
        if ($config->isStubGenerated()) {
445
            $logger->log(
446
                BuildLogger::QUESTION_MARK_PREFIX,
447
                'Generating new stub'
448
            );
449
450
            $stub = $this->createStub($config, $main, $checkRequirements, $logger);
451
452
            $box->getPhar()->setStub($stub->generate());
453
        } elseif (null !== ($stub = $config->getStubPath())) {
454
            $logger->log(
455
                BuildLogger::QUESTION_MARK_PREFIX,
456
                sprintf(
457
                    'Using stub file: %s',
458
                    $stub
459
                )
460
            );
461
462
            $box->registerStub($stub);
463
        } else {
464
            // TODO: add warning that the check requirements could not be added
465
            $aliasWasAdded = $box->getPhar()->setAlias($config->getAlias());
466
467
            Assertion::true(
468
                $aliasWasAdded,
469
                sprintf(
470
                    'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.',
471
                    $config->getAlias()
472
                )
473
            );
474
475
            $box->getPhar()->setDefaultStub($main);
476
477
            $logger->log(
478
                BuildLogger::QUESTION_MARK_PREFIX,
479
                'Using default stub'
480
            );
481
        }
482
    }
483
484
    private function configureMetadata(Configuration $config, Box $box, BuildLogger $logger): void
485
    {
486
        if (null !== ($metadata = $config->getMetadata())) {
487
            $logger->log(
488
                BuildLogger::QUESTION_MARK_PREFIX,
489
                'Setting metadata'
490
            );
491
492
            $logger->log(
493
                BuildLogger::MINUS_PREFIX,
494
                is_string($metadata) ? $metadata : var_export($metadata, true)
495
            );
496
497
            $box->getPhar()->setMetadata($metadata);
498
        }
499
    }
500
501
    private function configureCompressionAlgorithm(Configuration $config, Box $box, bool $dev, BuildLogger $logger): void
502
    {
503
        if (null !== ($algorithm = $config->getCompressionAlgorithm())) {
504
            $logger->log(
505
                BuildLogger::QUESTION_MARK_PREFIX,
506
                sprintf(
507
                    'Compressing with the algorithm "<comment>%s</comment>"',
508
                    array_search($algorithm, get_phar_compression_algorithms(), true)
1 ignored issue
show
Bug introduced by
It seems like array_search($algorithm,...ion_algorithms(), true) can also be of type false; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

508
                    /** @scrutinizer ignore-type */ array_search($algorithm, get_phar_compression_algorithms(), true)
Loading history...
509
                )
510
            );
511
512
            $box->getPhar()->compressFiles($algorithm);
513
        } else {
514
            $logger->log(
515
                BuildLogger::QUESTION_MARK_PREFIX,
516
                $dev
517
                    ? 'No compression'
518
                    : '<error>No compression</error>'
519
            );
520
        }
521
    }
522
523
    private function signPhar(
524
        Configuration $config,
525
        Box $box,
526
        string $path,
527
        InputInterface $input,
528
        OutputInterface $output,
529
        BuildLogger $logger
530
    ): void {
531
        // sign using private key, if applicable
532
        //TODO: check that out
533
        remove($path.'.pubkey');
534
535
        $key = $config->getPrivateKeyPath();
536
537
        if (null === $key) {
538
            if (null !== ($algorithm = $config->getSigningAlgorithm())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() is always true.
Loading history...
539
                $box->getPhar()->setSignatureAlgorithm($algorithm);
540
            }
541
542
            return;
543
        }
544
545
        $logger->log(
546
            BuildLogger::QUESTION_MARK_PREFIX,
547
            'Signing using a private key'
548
        );
549
550
        $passphrase = $config->getPrivateKeyPassphrase();
551
552
        if ($config->isPrivateKeyPrompt()) {
553
            if (false === $input->isInteractive()) {
554
                throw new RuntimeException(
555
                    sprintf(
556
                        'Accessing to the private key "%s" requires a passphrase but none provided. Either '
557
                        .'provide one or run this command in interactive mode.',
558
                        $key
559
                    )
560
                );
561
            }
562
563
            /** @var $dialog QuestionHelper */
564
            $dialog = $this->getHelper('question');
565
566
            $question = new Question('Private key passphrase:');
567
            $question->setHidden(false);
568
            $question->setHiddenFallback(false);
569
570
            $passphrase = $dialog->ask($input, $output, $question);
571
572
            $output->writeln('');
573
        }
574
575
        $box->signUsingFile($key, $passphrase);
576
    }
577
578
    private function correctPermissions(string $path, Configuration $config, BuildLogger $logger): void
579
    {
580
        if (null !== ($chmod = $config->getFileMode())) {
581
            $logger->log(
582
                BuildLogger::QUESTION_MARK_PREFIX,
583
                "Setting file permissions to <comment>$chmod</comment>"
584
            );
585
586
            chmod($path, $chmod);
587
        }
588
    }
589
590
    private function createStub(Configuration $config, ?string $main, bool $checkRequirements, BuildLogger $logger): StubGenerator
591
    {
592
        $stub = StubGenerator::create()
593
            ->alias($config->getAlias())
594
            ->index($main)
595
            ->intercept($config->isInterceptFileFuncs())
596
            ->checkRequirements($checkRequirements)
597
        ;
598
599
        if (null !== ($shebang = $config->getShebang())) {
600
            $logger->log(
601
                BuildLogger::MINUS_PREFIX,
602
                sprintf(
603
                    'Using shebang line: %s',
604
                    $shebang
605
                )
606
            );
607
608
            $stub->shebang($shebang);
609
        } else {
610
            $logger->log(
611
                BuildLogger::MINUS_PREFIX,
612
                'No shebang line'
613
            );
614
        }
615
616
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
617
            $logger->log(
618
                BuildLogger::MINUS_PREFIX,
619
                sprintf(
620
                    'Using custom banner from file: %s',
621
                    $bannerPath
622
                )
623
            );
624
625
            $stub->banner($config->getStubBannerContents());
626
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
627
            $logger->log(
628
                BuildLogger::MINUS_PREFIX,
629
                'Using banner:'
630
            );
631
632
            $bannerLines = explode("\n", $banner);
633
634
            foreach ($bannerLines as $bannerLine) {
635
                $logger->log(
636
                    BuildLogger::CHEVRON_PREFIX,
637
                    $bannerLine
638
                );
639
            }
640
641
            $stub->banner($banner);
642
        }
643
644
        return $stub;
645
    }
646
647
    private function logMap(MapFile $fileMapper, BuildLogger $logger): void
648
    {
649
        $map = $fileMapper->getMap();
650
651
        if ([] === $map) {
652
            return;
653
        }
654
655
        $logger->log(
656
            BuildLogger::QUESTION_MARK_PREFIX,
657
            'Mapping paths'
658
        );
659
660
        foreach ($map as $item) {
661
            foreach ($item as $match => $replace) {
662
                if ('' === $match) {
663
                    $match = '(all)';
664
                    $replace .= '/';
665
                }
666
667
                $logger->log(
668
                    BuildLogger::MINUS_PREFIX,
669
                    sprintf(
670
                        '%s <info>></info> %s',
671
                        $match,
672
                        $replace
673
                    )
674
                );
675
            }
676
        }
677
    }
678
679
    private function createScoper(): SimpleScoper
0 ignored issues
show
Unused Code introduced by
The method createScoper() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
680
    {
681
        return new SimpleScoper(
682
            new PhpScoper(
683
                (new ParserFactory())->create(ParserFactory::ONLY_PHP5),
684
                new NullScoper(),
685
                new TraverserFactory(create_reflector())
686
            ),
687
            '_HumbugBox',
688
            [],
689
            []
690
        );
691
    }
692
}
693