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

Compile::signPhar()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 53
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 53
rs 8.7155
c 0
b 0
f 0
cc 5
eloc 24
nc 5
nop 6

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\enable_debug;
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 KevinGH\Box\is_debug_enabled;
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 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
360
    {
361
        $main = $config->getMainScriptPath();
362
363
        $logger->log(
364
            BuildLogger::QUESTION_MARK_PREFIX,
365
            sprintf(
366
                'Adding main file: %s',
367
                $main
368
            )
369
        );
370
371
        $localMain = $box->addFile(
372
            $main,
373
            $config->getMainScriptContents()
374
        );
375
376
        $relativeMain = make_path_relative($main, $config->getBasePath());
377
378
        if ($localMain !== $relativeMain) {
379
            $logger->log(
380
                BuildLogger::CHEVRON_PREFIX,
381
                $localMain
382
            );
383
        }
384
385
        return $localMain;
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();
0 ignored issues
show
Unused Code introduced by
The assignment to $scoper is dead and can be removed.
Loading history...
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
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

480
                    /** @scrutinizer ignore-type */ array_search($algorithm, get_phar_compression_algorithms(), true)
Loading history...
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())) {
0 ignored issues
show
introduced by
The condition null !== $algorithm = $c...->getSigningAlgorithm() is always true.
Loading history...
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
563
    {
564
        $stub = StubGenerator::create()
565
            ->alias($config->getAlias())
566
            ->index($main)
567
            ->intercept($config->isInterceptFileFuncs())
568
            ->checkRequirements($checkRequirements)
569
        ;
570
571
        if (null !== ($shebang = $config->getShebang())) {
572
            $logger->log(
573
                BuildLogger::MINUS_PREFIX,
574
                sprintf(
575
                    'Using shebang line: %s',
576
                    $shebang
577
                )
578
            );
579
580
            $stub->shebang($shebang);
581
        } else {
582
            $logger->log(
583
                BuildLogger::MINUS_PREFIX,
584
                'No shebang line'
585
            );
586
        }
587
588
        if (null !== ($bannerPath = $config->getStubBannerPath())) {
589
            $logger->log(
590
                BuildLogger::MINUS_PREFIX,
591
                sprintf(
592
                    'Using custom banner from file: %s',
593
                    $bannerPath
594
                )
595
            );
596
597
            $stub->banner($config->getStubBannerContents());
598
        } elseif (null !== ($banner = $config->getStubBannerContents())) {
599
            $logger->log(
600
                BuildLogger::MINUS_PREFIX,
601
                'Using banner:'
602
            );
603
604
            $bannerLines = explode("\n", $banner);
605
606
            foreach ($bannerLines as $bannerLine) {
607
                $logger->log(
608
                    BuildLogger::CHEVRON_PREFIX,
609
                    $bannerLine
610
                );
611
            }
612
613
            $stub->banner($banner);
614
        }
615
616
        return $stub;
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
652
    {
653
        return new SimpleScoper(
654
            new PhpScoper(
655
                (new ParserFactory())->create(ParserFactory::ONLY_PHP5),
656
                new NullScoper(),
657
                new TraverserFactory(create_reflector())
658
            ),
659
            '_HumbugBox',
660
            [],
661
            []
662
        );
663
    }
664
}
665