Completed
Pull Request — master (#31)
by Théo
02:18
created

Configuration::retrievePhpScoperConfig()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
eloc 14
nc 4
nop 2
cc 4
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;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use const DIRECTORY_SEPARATOR;
21
use function explode;
22
use Herrera\Annotations\Tokenizer;
23
use Herrera\Box\Compactor\Php as LegacyPhp;
24
use Humbug\PhpScoper\Console\Configuration as PhpScoperConfiguration;
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\Console\Configuration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use function implode;
26
use InvalidArgumentException;
27
use function is_bool;
28
use function is_array;
29
use function is_string;
30
use KevinGH\Box\Compactor\Php;
31
use KevinGH\Box\Compactor\PhpScoper;
32
use Phar;
33
use RuntimeException;
34
use SplFileInfo;
35
use stdClass;
36
use Symfony\Component\Finder\Finder;
37
use Symfony\Component\Process\Process;
38
use function Humbug\PhpScoper\create_scoper;
0 ignored issues
show
introduced by
The function Humbug\PhpScoper\create_scoper was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
39
use function iter\chain;
40
use function KevinGH\Box\FileSystem\canonicalize;
41
use function KevinGH\Box\FileSystem\file_contents;
42
use function KevinGH\Box\FileSystem\is_absolute_path;
43
use function KevinGH\Box\FileSystem\make_path_absolute;
44
45
final class Configuration
46
{
47
    private const DEFAULT_ALIAS = 'default.phar';
48
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
49
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
50
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
51
52
    private $fileMode;
53
    private $alias;
54
    private $basePathRetriever;
55
    private $files;
56
    private $binaryFiles;
57
    private $bootstrapFile;
58
    private $compactors;
59
    private $compressionAlgorithm;
60
    private $mainScriptPath;
61
    private $mainScriptContent;
62
    private $map;
63
    private $fileMapper;
64
    private $metadata;
65
    private $outputPath;
66
    private $privateKeyPassphrase;
67
    private $privateKeyPath;
68
    private $isPrivateKeyPrompt;
69
    private $processedReplacements;
70
    private $shebang;
71
    private $signingAlgorithm;
72
    private $stubBannerContents;
73
    private $stubBannerPath;
74
    private $stubPath;
75
    private $isInterceptFileFuncs;
76
    private $isStubGenerated;
77
78
    /**
79
     * @param string|null                   $alias
80
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
81
     * @param SplFileInfo[]            $files                 List of files
82
     * @param SplFileInfo[]            $binaryFiles           List of binary files
83
     * @param null|string              $bootstrapFile         The bootstrap file path
84
     * @param Compactor[]              $compactors            List of file contents compactors
85
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
86
     * @param null|int                 $fileMode              File mode in octal form
87
     * @param null|string              $mainScriptPath        The main script file path
88
     * @param null|string              $mainScriptContent     The processed content of the main script file
89
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
90
     * @param mixed                    $metadata              The PHAR Metadata
91
     * @param string                   $outputPath
92
     * @param null|string              $privateKeyPassphrase
93
     * @param null|string              $privateKeyPath
94
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
95
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
96
     * @param null|string              $shebang               The shebang line
97
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
98
     * @param string|null              $stubBannerContents            The stub banner comment
99
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
100
     * @param null|string              $stubPath              The PHAR stub file path
101
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
102
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
103
     */
104
    private function __construct(
105
        ?string $alias,
106
        RetrieveRelativeBasePath $basePathRetriever,
107
        array $files,
108
        array $binaryFiles,
109
        ?string $bootstrapFile,
110
        array $compactors,
111
        ?int $compressionAlgorithm,
112
        ?int $fileMode,
113
        ?string $mainScriptPath,
114
        ?string $mainScriptContent,
115
        MapFile $fileMapper,
116
        $metadata,
117
        string $outputPath,
118
        ?string $privateKeyPassphrase,
119
        ?string $privateKeyPath,
120
        bool $isPrivateKeyPrompt,
121
        array $processedReplacements,
122
        ?string $shebang,
123
        int $signingAlgorithm,
124
        ?string $stubBannerContents,
125
        ?string $stubBannerPath,
126
        ?string $stubPath,
127
        bool $isInterceptFileFuncs,
128
        bool $isStubGenerated
129
    ) {
130
        Assertion::nullOrInArray(
131
            $compressionAlgorithm,
132
            get_phar_compression_algorithms(),
133
            sprintf(
134
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
135
                implode('", "', array_keys(get_phar_compression_algorithms()))
136
            )
137
        );
138
139
        $this->alias = $alias;
140
        $this->basePathRetriever = $basePathRetriever;
141
        $this->files = $files;
142
        $this->binaryFiles = $binaryFiles;
143
        $this->bootstrapFile = $bootstrapFile;
144
        $this->compactors = $compactors;
145
        $this->compressionAlgorithm = $compressionAlgorithm;
146
        $this->fileMode = $fileMode;
147
        $this->mainScriptPath = $mainScriptPath;
148
        $this->mainScriptContent = $mainScriptContent;
149
        $this->fileMapper = $fileMapper;
150
        $this->metadata = $metadata;
151
        $this->outputPath = $outputPath;
152
        $this->privateKeyPassphrase = $privateKeyPassphrase;
153
        $this->privateKeyPath = $privateKeyPath;
154
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
155
        $this->processedReplacements = $processedReplacements;
156
        $this->shebang = $shebang;
157
        $this->signingAlgorithm = $signingAlgorithm;
158
        $this->stubBannerContents = $stubBannerContents;
159
        $this->stubBannerPath = $stubBannerPath;
160
        $this->stubPath = $stubPath;
161
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
162
        $this->isStubGenerated = $isStubGenerated;
163
    }
164
165
    public static function create(string $file, stdClass $raw): self
166
    {
167
        $alias = self::retrieveAlias($raw);
168
169
        $basePath = self::retrieveBasePath($file, $raw);
170
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
171
172
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
173
174
        $files = self::retrieveFiles($raw, 'files', $basePath);
175
        $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
176
        $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
177
178
        $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
179
180
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
181
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
182
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter);
183
184
        $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
185
186
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
187
188
        $compactors = self::retrieveCompactors($raw, $basePath);
189
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
190
191
        $fileMode = self::retrieveFileMode($raw);
192
193
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
194
        $mainScriptContent = self::retrieveMainScriptContents($mainScriptPath);
195
196
        $map = self::retrieveMap($raw);
197
        $fileMapper = new MapFile($map);
198
199
        $metadata = self::retrieveMetadata($raw);
200
201
        $outputPath = self::retrieveOutputPath($raw, $file);
202
203
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
204
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
205
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
206
207
        $replacements = self::retrieveReplacements($raw);
208
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
209
210
        $shebang = self::retrieveShebang($raw);
211
212
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
213
214
        $stubBannerContents = self::retrieveStubBannerContents($raw);
215
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
216
217
        if (null !== $stubBannerPath) {
218
            $stubBannerContents = file_contents($stubBannerPath);
219
        }
220
221
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
222
223
        $stubPath = self::retrieveStubPath($raw, $basePath);
224
225
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
226
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
227
228
        return new self(
229
            $alias,
230
            $basePathRetriever,
231
            $filesAggregate,
232
            $binaryFilesAggregate,
233
            $bootstrapFile,
234
            $compactors,
235
            $compressionAlgorithm,
236
            $fileMode,
237
            $mainScriptPath,
238
            $mainScriptContent,
239
            $fileMapper,
240
            $metadata,
241
            $outputPath,
242
            $privateKeyPassphrase,
243
            $privateKeyPath,
244
            $isPrivateKeyPrompt,
245
            $processedReplacements,
246
            $shebang,
247
            $signingAlgorithm,
248
            $stubBannerContents,
249
            $stubBannerPath,
250
            $stubPath,
251
            $isInterceptFileFuncs,
252
            $isStubGenerated
253
        );
254
    }
255
256
    public function getBasePathRetriever(): RetrieveRelativeBasePath
257
    {
258
        return $this->basePathRetriever;
259
    }
260
261
    public function getAlias(): ?string
262
    {
263
        return $this->alias;
264
    }
265
266
    public function getBasePath(): string
267
    {
268
        return $this->basePathRetriever->getBasePath();
269
    }
270
271
    /**
272
     * @return SplFileInfo[]
273
     */
274
    public function getFiles(): array
275
    {
276
        return $this->files;
277
    }
278
279
    /**
280
     * @return SplFileInfo[]
281
     */
282
    public function getBinaryFiles(): array
283
    {
284
        return $this->binaryFiles;
285
    }
286
287
    public function getBootstrapFile(): ?string
288
    {
289
        return $this->bootstrapFile;
290
    }
291
292
    public function loadBootstrap(): void
293
    {
294
        $file = $this->bootstrapFile;
295
296
        if (null !== $file) {
297
            include $file;
298
        }
299
    }
300
301
    /**
302
     * @return Compactor[] the list of compactors
303
     */
304
    public function getCompactors(): array
305
    {
306
        return $this->compactors;
307
    }
308
309
    public function getCompressionAlgorithm(): ?int
310
    {
311
        return $this->compressionAlgorithm;
312
    }
313
314
    public function getFileMode(): ?int
315
    {
316
        return $this->fileMode;
317
    }
318
319
    public function getMainScriptPath(): ?string
320
    {
321
        return $this->mainScriptPath;
322
    }
323
324
    public function getMainScriptContent(): ?string
325
    {
326
        return $this->mainScriptContent;
327
    }
328
329
    public function getOutputPath(): string
330
    {
331
        return $this->outputPath;
332
    }
333
334
    /**
335
     * @return string[]
336
     */
337
    public function getMap(): array
338
    {
339
        return $this->fileMapper->getMap();
340
    }
341
342
    public function getFileMapper(): MapFile
343
    {
344
        return $this->fileMapper;
345
    }
346
347
    /**
348
     * @return mixed
349
     */
350
    public function getMetadata()
351
    {
352
        return $this->metadata;
353
    }
354
355
    public function getPrivateKeyPassphrase(): ?string
356
    {
357
        return $this->privateKeyPassphrase;
358
    }
359
360
    public function getPrivateKeyPath(): ?string
361
    {
362
        return $this->privateKeyPath;
363
    }
364
365
    public function isPrivateKeyPrompt(): bool
366
    {
367
        return $this->isPrivateKeyPrompt;
368
    }
369
370
    public function getProcessedReplacements(): array
371
    {
372
        return $this->processedReplacements;
373
    }
374
375
    public function getShebang(): ?string
376
    {
377
        return $this->shebang;
378
    }
379
380
    public function getSigningAlgorithm(): int
381
    {
382
        return $this->signingAlgorithm;
383
    }
384
385
    public function getStubBannerContents(): ?string
386
    {
387
        return $this->stubBannerContents;
388
    }
389
390
    public function getStubBannerPath(): ?string
391
    {
392
        return $this->stubBannerPath;
393
    }
394
395
    public function getStubPath(): ?string
396
    {
397
        return $this->stubPath;
398
    }
399
400
    public function isInterceptFileFuncs(): bool
401
    {
402
        return $this->isInterceptFileFuncs;
403
    }
404
405
    public function isStubGenerated(): bool
406
    {
407
        return $this->isStubGenerated;
408
    }
409
410
    private static function retrieveAlias(stdClass $raw): ?string
411
    {
412
        if (false === isset($raw->alias)) {
413
            return null;
414
        }
415
416
        $alias = trim($raw->alias);
417
418
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
419
420
        return $alias;
421
    }
422
423
    private static function retrieveBasePath(string $file, stdClass $raw): string
424
    {
425
        if (false === isset($raw->{'base-path'})) {
426
            return realpath(dirname($file));
427
        }
428
429
        $basePath = trim($raw->{'base-path'});
430
431
        Assertion::directory(
432
            $basePath,
433
            'The base path "%s" is not a directory or does not exist.'
434
        );
435
436
        return realpath($basePath);
437
    }
438
439
    /**
440
     * @param stdClass $raw
441
     * @param string   $basePath
442
     *
443
     * @return Closure
444
     */
445
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
446
    {
447
        $blacklist = self::retrieveBlacklist($raw, $basePath);
448
449
        return function (SplFileInfo $file) use ($blacklist): ?bool {
450
            if (in_array($file->getRealPath(), $blacklist, true)) {
451
                return false;
452
            }
453
454
            return null;
455
        };
456
    }
457
458
    /**
459
     * @return string[]
460
     */
461
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
462
    {
463
        if (false === isset($raw->blacklist)) {
464
            return [];
465
        }
466
467
        $blacklist = $raw->blacklist;
468
469
        $normalizePath = function ($file) use ($basePath): string {
470
            return self::normalizeFilePath($file, $basePath);
471
        };
472
473
        return array_map($normalizePath, $blacklist);
474
    }
475
476
    /**
477
     * @param stdClass $raw
478
     * @param string   $key      Config property name
479
     * @param string   $basePath
480
     *
481
     * @return SplFileInfo[]
482
     */
483
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
484
    {
485
        if (false === isset($raw->{$key})) {
486
            return [];
487
        }
488
489
        $files = (array) $raw->{$key};
490
491
        Assertion::allString($files);
492
493
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
494
            $file = self::normalizeFilePath($file, $basePath);
495
496
            Assertion::file(
497
                $file,
498
                sprintf(
499
                    '"%s" must contain a list of existing files. Could not find "%%s".',
500
                    $key
501
                )
502
            );
503
504
            return new SplFileInfo($file);
505
        };
506
507
        return array_map($normalizePath, $files);
508
    }
509
510
    /**
511
     * @param stdClass $raw
512
     * @param string   $key             Config property name
513
     * @param string   $basePath
514
     * @param Closure  $blacklistFilter
515
     *
516
     * @return iterable|SplFileInfo[]
517
     */
518
    private static function retrieveDirectories(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): iterable
519
    {
520
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
521
522
        if ([] !== $directories) {
523
            return Finder::create()
1 ignored issue
show
Bug Best Practice introduced by
The expression return Symfony\Component...true)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
524
                ->files()
525
                ->filter($blacklistFilter)
526
                ->ignoreVCS(true)
527
                ->in($directories)
528
            ;
529
        }
530
531
        return [];
532
    }
533
534
    /**
535
     * @param stdClass $raw
536
     * @param string   $basePath
537
     * @param Closure  $blacklistFilter
538
     *
539
     * @return iterable[]|SplFileInfo[][]
540
     */
541
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
542
    {
543
        if (isset($raw->{$key})) {
544
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
545
        }
546
547
        return [];
548
    }
549
550
    /**
551
     * @param array   $findersConfig
552
     * @param string  $basePath
553
     * @param Closure $blacklistFilter
554
     *
555
     * @return Finder[]|SplFileInfo[][]
556
     */
557
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
558
    {
559
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter) {
560
            return self::processFinder($config, $basePath, $blacklistFilter);
561
        };
562
563
        return array_map($processFinderConfig, $findersConfig);
564
    }
565
566
    /**
567
     * @param stdClass $config
568
     * @param string $basePath
569
     * @param Closure $blacklistFilter
570
     *
571
     * @return Finder
572
     */
573
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter): Finder
574
    {
575
        $finder = Finder::create()
576
            ->files()
577
            ->filter($blacklistFilter)
578
            ->ignoreVCS(true)
579
        ;
580
581
        $normalizedConfig = (function (array $config, Finder $finder): array {
582
            $normalizedConfig = [];
583
584
            foreach ($config as $method => $arguments) {
585
                $method = trim($method);
586
                $arguments = (array) $arguments;
587
588
                Assertion::methodExists(
589
                    $method,
590
                    $finder,
591
                    'The method "Finder::%s" does not exist.'
592
                );
593
594
                $normalizedConfig[$method] = $arguments;
595
            }
596
597
            krsort($normalizedConfig);
598
599
            return $normalizedConfig;
600
        })((array) $config, $finder);
601
602
        $createNormalizedDirectories = function (string $directory) use ($basePath): string {
603
            $directory = self::normalizeDirectoryPath($directory, $basePath);
604
605
            Assertion::directory($directory);
606
607
            return $directory;
608
        };
609
610
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
611
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
612
613
            if (false === file_exists($fileOrDirectory)) {
614
                throw new InvalidArgumentException(
615
                    sprintf(
616
                        'Path "%s" was expected to be a file or directory.',
617
                        $fileOrDirectory
618
                    )
619
                );
620
            }
621
622
            //TODO: add fileExists (as file or directory) to Assert
623
            if (false === is_file($fileOrDirectory)) {
624
                Assertion::directory($fileOrDirectory);
625
            } else {
626
                Assertion::file($fileOrDirectory);
627
            }
628
        };
629
630
        foreach ($normalizedConfig as $method => $arguments) {
631
            if ('in' === $method) {
632
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
633
            }
634
635
            if ('exclude' === $method) {
636
                $arguments = array_unique(array_map('trim', $arguments));
637
            }
638
639
            if ('append' === $method) {
640
                array_walk($arguments, $normalizeFileOrDirectory);
641
642
                $arguments = [$arguments];
643
            }
644
645
            foreach ($arguments as $argument) {
646
                $finder->$method($argument);
647
            }
648
        }
649
650
        return $finder;
651
    }
652
653
    /**
654
     * @param stdClass $raw
655
     * @param string   $key      Config property name
656
     * @param string   $basePath
657
     *
658
     * @return string[]
659
     */
660
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
661
    {
662
        if (false === isset($raw->{$key})) {
663
            return [];
664
        }
665
666
        $directories = $raw->{$key};
667
668
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
669
            $directory = self::normalizeDirectoryPath($directory, $basePath);
670
671
            Assertion::directory(
672
                $directory,
673
                sprintf(
674
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
675
                    $key
676
                )
677
            );
678
679
            return $directory;
680
        };
681
682
        return array_map($normalizeDirectory, $directories);
683
    }
684
685
    private static function normalizeFilePath(string $file, string $basePath): string
686
    {
687
        return make_path_absolute(trim($file), $basePath);
688
    }
689
690
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
691
    {
692
        return make_path_absolute(trim($directory), $basePath);
693
    }
694
695
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
696
    {
697
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
698
        // through that extension point so this is pretty much useless unless proven otherwise.
699
        if (false === isset($raw->bootstrap)) {
700
            return null;
701
        }
702
703
        $file = self::normalizeFilePath($raw->bootstrap, $basePath);
704
705
        Assertion::file($file, 'The bootstrap path "%s" is not a file or does not exist.');
706
707
        return $file;
708
    }
709
710
    /**
711
     * @return Compactor[]
712
     */
713
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
714
    {
715
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
716
        if (false === isset($raw->compactors)) {
717
            return [];
718
        }
719
720
        $compactorClasses = array_unique((array) $raw->compactors);
721
722
        return array_map(
723
            function (string $class) use ($raw, $basePath): Compactor {
724
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
725
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
726
727
                if (Php::class === $class || LegacyPhp::class === $class) {
728
                    return self::createPhpCompactor($raw);
729
                }
730
731
                if (PhpScoper::class === $class) {
732
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
733
734
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
0 ignored issues
show
Bug introduced by
The function create_scoper was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

734
                    return new PhpScoper(/** @scrutinizer ignore-call */ create_scoper(), $phpScoperConfig);
Loading history...
735
                }
736
737
                return new $class();
738
            },
739
            $compactorClasses
740
        );
741
    }
742
743
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
744
    {
745
        // TODO: if in dev mode (when added), do not comment about the compression.
746
        // If not, add a warning to notify the user if no compression algorithm is used
747
        // provided the PHAR is not configured for web purposes.
748
        // If configured for the web, add a warning when a compression algorithm is used
749
        // as this can result in an overhead. Add a doc link explaining this.
750
        //
751
        // Unlike the doc: do not accept integers and document this BC break.
752
        if (false === isset($raw->compression)) {
753
            return null;
754
        }
755
756
        if (false === is_string($raw->compression)) {
757
            Assertion::integer(
758
                $raw->compression,
759
                'Expected compression to be an algorithm name, found %s instead.'
760
            );
761
762
            return $raw->compression;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $raw->compression returns the type string which is incompatible with the type-hinted return null|integer.
Loading history...
763
        }
764
765
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
766
767
        Assertion::inArray(
768
            $raw->compression,
769
            $knownAlgorithmNames,
770
            sprintf(
771
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
772
                implode('", "', $knownAlgorithmNames)
773
            )
774
        );
775
776
        $value = get_phar_compression_algorithms()[$raw->compression];
777
778
        // Phar::NONE is not valid for compressFiles()
779
        if (Phar::NONE === $value) {
780
            return null;
781
        }
782
783
        return $value;
784
    }
785
786
    private static function retrieveFileMode(stdClass $raw): ?int
787
    {
788
        if (isset($raw->chmod)) {
789
            return intval($raw->chmod, 8);
790
        }
791
792
        return null;
793
    }
794
795
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): ?string
796
    {
797
        if (isset($raw->main)) {
798
//            return canonicalize($raw->main);
799
            return make_path_absolute($raw->main, $basePath);
800
        }
801
802
        return null;
803
    }
804
805
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
806
    {
807
        if (null === $mainScriptPath) {
808
            return null;
809
        }
810
811
        $contents = file_contents($mainScriptPath);
812
813
        // Remove the shebang line
814
        return preg_replace('/^#!.*\s*/', '', $contents);
815
    }
816
817
    /**
818
     * @return string[][]
819
     */
820
    private static function retrieveMap(stdClass $raw): array
821
    {
822
        if (false === isset($raw->map)) {
823
            return [];
824
        }
825
826
        $map = [];
827
828
        foreach ((array) $raw->map as $item) {
829
            $processed = [];
830
831
            foreach ($item as $match => $replace) {
832
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
833
            }
834
835
            if (isset($processed['_empty_'])) {
836
                $processed[''] = $processed['_empty_'];
837
838
                unset($processed['_empty_']);
839
            }
840
841
            $map[] = $processed;
842
        }
843
844
        return $map;
845
    }
846
847
    /**
848
     * @return mixed
849
     */
850
    private static function retrieveMetadata(stdClass $raw)
851
    {
852
        // TODO: the doc currently say this can be any value; check if true
853
        // and if not add checks accordingly
854
        //
855
        // Also review the doc as I don't find it very helpful...
856
        if (isset($raw->metadata)) {
857
            if (is_object($raw->metadata)) {
858
                return (array) $raw->metadata;
859
            }
860
861
            return $raw->metadata;
862
        }
863
864
        return null;
865
    }
866
867
    private static function retrieveOutputPath(stdClass $raw, string $file): string
868
    {
869
        // TODO: make this path relative to the base path like everything else
870
        // otherwise this is really confusing. This is a BC break that needs to be
871
        // documented though (and update the doc accordingly as well)
872
        $base = getcwd().DIRECTORY_SEPARATOR;
873
874
        if (isset($raw->output)) {
875
            $path = $raw->output;
876
877
            if (false === is_absolute_path($path)) {
878
                $path = canonicalize($base.$path);
879
            }
880
        } else {
881
            $path = $base.self::DEFAULT_ALIAS;
882
        }
883
884
        if (false !== strpos($path, '@'.'git-version@')) {
885
            $gitVersion = self::retrieveGitVersion($file);
886
887
            $path = str_replace('@'.'git-version@', $gitVersion, $path);
888
        }
889
890
        return $path;
891
    }
892
893
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
894
    {
895
        // TODO: add check to not allow this setting without the private key path
896
        if (isset($raw->{'key-pass'})
897
            && is_string($raw->{'key-pass'})
898
        ) {
899
            return $raw->{'key-pass'};
900
        }
901
902
        return null;
903
    }
904
905
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
906
    {
907
        // TODO: If passed need to check its existence
908
        // Also need
909
910
        if (isset($raw->key)) {
911
            return $raw->key;
912
        }
913
914
        return null;
915
    }
916
917
    private static function retrieveReplacements(stdClass $raw): array
918
    {
919
        // TODO: add exmample in the doc
920
        // Add checks against the values
921
        if (isset($raw->replacements)) {
922
            return (array) $raw->replacements;
923
        }
924
925
        return [];
926
    }
927
928
    private static function retrieveProcessedReplacements(
929
        array $replacements,
930
        stdClass $raw,
931
        string $file
932
    ): array {
933
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
934
            $replacements[$git] = self::retrieveGitHash($file);
935
        }
936
937
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
938
            $replacements[$git] = self::retrieveGitHash($file, true);
939
        }
940
941
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
942
            $replacements[$git] = self::retrieveGitTag($file);
943
        }
944
945
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
946
            $replacements[$git] = self::retrieveGitVersion($file);
947
        }
948
949
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
950
            $replacements[$date] = self::retrieveDatetimeNow(
951
                self::retrieveDatetimeFormat($raw)
952
            );
953
        }
954
955
        $sigil = self::retrieveReplacementSigil($raw);
956
957
        foreach ($replacements as $key => $value) {
958
            unset($replacements[$key]);
959
            $replacements["$sigil$key$sigil"] = $value;
960
        }
961
962
        return $replacements;
963
    }
964
965
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
966
    {
967
        if (isset($raw->{'git-commit'})) {
968
            return $raw->{'git-commit'};
969
        }
970
971
        return null;
972
    }
973
974
    /**
975
     * @param string $file
976
     * @param bool   $short Use the short version
977
     *
978
     * @return string the commit hash
979
     */
980
    private static function retrieveGitHash(string $file, bool $short = false): string
981
    {
982
        return self::runGitCommand(
983
            sprintf(
984
                'git log --pretty="%s" -n1 HEAD',
985
                $short ? '%h' : '%H'
986
            ),
987
            $file
988
        );
989
    }
990
991
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
992
    {
993
        if (isset($raw->{'git-commit-short'})) {
994
            return $raw->{'git-commit-short'};
995
        }
996
997
        return null;
998
    }
999
1000
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1001
    {
1002
        if (isset($raw->{'git-tag'})) {
1003
            return $raw->{'git-tag'};
1004
        }
1005
1006
        return null;
1007
    }
1008
1009
    private static function retrieveGitTag(string $file): ?string
1010
    {
1011
        return self::runGitCommand('git describe --tags HEAD', $file);
1012
    }
1013
1014
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1015
    {
1016
        if (isset($raw->{'git-version'})) {
1017
            return $raw->{'git-version'};
1018
        }
1019
1020
        return null;
1021
    }
1022
1023
    private static function retrieveGitVersion(string $file): ?string
1024
    {
1025
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1026
        // to avoid messing around with that
1027
1028
        try {
1029
            return self::retrieveGitTag($file);
1030
        } catch (RuntimeException $exception) {
1031
            try {
1032
                return self::retrieveGitHash($file, true);
1033
            } catch (RuntimeException $exception) {
1034
                throw new RuntimeException(
1035
                    sprintf(
1036
                        'The tag or commit hash could not be retrieved from "%s": %s',
1037
                        dirname($file),
1038
                        $exception->getMessage()
1039
                    ),
1040
                    0,
1041
                    $exception
1042
                );
1043
            }
1044
        }
1045
    }
1046
1047
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1048
    {
1049
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1050
        // Also make sure the documentation is up to date after.
1051
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1052
        // be better to have only one element IMO like:
1053
        //
1054
        // "datetime": {
1055
        //   "value": "val",
1056
        //   "format": "Y-m-d"
1057
        // }
1058
        //
1059
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1060
        // if the datetime format is the default one it's ok; but in any case the format should not
1061
        // be added without the datetime value...
1062
1063
        if (isset($raw->{'datetime'})) {
1064
            return $raw->{'datetime'};
1065
        }
1066
1067
        return null;
1068
    }
1069
1070
    private static function retrieveDatetimeNow(string $format)
1071
    {
1072
        $now = new DateTimeImmutable('now');
1073
1074
        $datetime = $now->format($format);
1075
1076
        if (!$datetime) {
1077
            throw new InvalidArgumentException(
1078
                sprintf(
1079
                    '""%s" is not a valid PHP date format',
1080
                    $format
1081
                )
1082
            );
1083
        }
1084
1085
        return $datetime;
1086
    }
1087
1088
    private static function retrieveDatetimeFormat(stdClass $raw): string
1089
    {
1090
        if (isset($raw->{'datetime_format'})) {
1091
            return $raw->{'datetime_format'};
1092
        }
1093
1094
        return self::DEFAULT_DATETIME_FORMAT;
1095
    }
1096
1097
    private static function retrieveReplacementSigil(stdClass $raw)
1098
    {
1099
        if (isset($raw->{'replacement-sigil'})) {
1100
            return $raw->{'replacement-sigil'};
1101
        }
1102
1103
        return self::DEFAULT_REPLACEMENT_SIGIL;
1104
    }
1105
1106
    private static function retrieveShebang(stdClass $raw): ?string
1107
    {
1108
        if (false === isset($raw->shebang)) {
1109
            return null;
1110
        }
1111
1112
        $shebang = trim($raw->shebang);
1113
1114
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1115
        Assertion::true(
1116
            '#!' === substr($shebang, 0, 2),
1117
            sprintf(
1118
                'The shebang line must start with "#!". Got "%s" instead',
1119
                $shebang
1120
            )
1121
        );
1122
1123
        return $shebang;
1124
    }
1125
1126
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1127
    {
1128
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1129
        // TODO: trigger a warning if the signing algorithm used is weak
1130
        // TODO: no longer accept strings & document BC break
1131
        if (false === isset($raw->algorithm)) {
1132
            return Phar::SHA1;
1133
        }
1134
1135
        if (is_string($raw->algorithm)) {
1136
            if (false === defined('Phar::'.$raw->algorithm)) {
1137
                throw new InvalidArgumentException(
1138
                    sprintf(
1139
                        'The signing algorithm "%s" is not supported.',
1140
                        $raw->algorithm
1141
                    )
1142
                );
1143
            }
1144
1145
            return constant('Phar::'.$raw->algorithm);
1146
        }
1147
1148
        return $raw->algorithm;
1149
    }
1150
1151
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1152
    {
1153
        if (false === isset($raw->banner)) {
1154
            return null;
1155
        }
1156
1157
        $banner = $raw->banner;
1158
1159
        if (is_array($banner)) {
1160
            $banner = implode("\n", $banner);
1161
        }
1162
1163
        return $banner;
1164
    }
1165
1166
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1167
    {
1168
        if (false === isset($raw->{'banner-file'})) {
1169
            return null;
1170
        }
1171
1172
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1173
1174
        Assertion::file($bannerFile);
1175
1176
        return $bannerFile;
1177
    }
1178
1179
    private static function normalizeStubBannerContents(?string $contents): ?string
1180
    {
1181
        if (null === $contents) {
1182
            return null;
1183
        }
1184
1185
        $banner = explode("\n", $contents);
1186
        $banner = array_map('trim', $banner);
1187
1188
        return implode("\n", $banner);
1189
    }
1190
1191
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1192
    {
1193
        if (isset($raw->stub) && is_string($raw->stub)) {
1194
            $stubPath = make_path_absolute($raw->stub, $basePath);
1195
1196
            Assertion::file($stubPath);
1197
1198
            return $stubPath;
1199
        }
1200
1201
        return null;
1202
    }
1203
1204
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1205
    {
1206
        if (isset($raw->intercept)) {
1207
            return $raw->intercept;
1208
        }
1209
1210
        return false;
1211
    }
1212
1213
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1214
    {
1215
        if (isset($raw->{'key-pass'})
1216
            && (true === $raw->{'key-pass'})) {
1217
            return true;
1218
        }
1219
1220
        return false;
1221
    }
1222
1223
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1224
    {
1225
        if (isset($raw->stub) && (true === $raw->stub)) {
1226
            return true;
1227
        }
1228
1229
        return false;
1230
    }
1231
1232
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1233
    {
1234
        if (!isset($raw->{'php-scoper'})) {
1235
            $configFilePath = $basePath.DIRECTORY_SEPARATOR.self::PHP_SCOPER_CONFIG;
1236
1237
             return file_exists($configFilePath)
1238
                ? PhpScoperConfiguration::load($configFilePath)
1239
                : PhpScoperConfiguration::load()
1240
             ;
1241
        }
1242
1243
        $configFile = $raw->phpScoper;
1244
1245
        Assertion::string($configFile);
1246
1247
        if (false === self::$fileSystem->isAbsolutePath($configFile)) {
0 ignored issues
show
Bug Best Practice introduced by
The property fileSystem does not exist on KevinGH\Box\Configuration. Did you maybe forget to declare it?
Loading history...
1248
            $configFilePath = $basePath.DIRECTORY_SEPARATOR.$configFile;
1249
        } else {
1250
            $configFilePath = $configFile;
1251
        }
1252
1253
        Assertion::file($configFilePath);
1254
        Assertion::readable($configFilePath);
1255
1256
        return PhpScoperConfiguration::load($configFilePath);
1257
    }
1258
1259
    /**
1260
     * Runs a Git command on the repository.
1261
     *
1262
     * @param string $command the command
1263
     *
1264
     * @return string the trimmed output from the command
1265
     */
1266
    private static function runGitCommand(string $command, string $file): string
1267
    {
1268
        $path = dirname($file);
1269
1270
        $process = new Process($command, $path);
1271
1272
        if (0 === $process->run()) {
1273
            return trim($process->getOutput());
1274
        }
1275
1276
        throw new RuntimeException(
1277
            sprintf(
1278
                'The tag or commit hash could not be retrieved from "%s": %s',
1279
                $path,
1280
                $process->getErrorOutput()
1281
            )
1282
        );
1283
    }
1284
1285
    private static function createPhpCompactor(stdClass $raw): Compactor
1286
    {
1287
        // TODO: false === not set; check & add test/doc
1288
        $tokenizer = new Tokenizer();
1289
1290
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1291
            $tokenizer->ignore(
1292
                (array) $raw->annotations->ignore
1293
            );
1294
        }
1295
1296
        return new Php($tokenizer);
1297
    }
1298
}
1299