Passed
Pull Request — master (#88)
by Théo
02:23
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 9.5555
c 0
b 0
f 0
cc 1
eloc 30
nc 1
nop 25

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 Herrera\Annotations\Tokenizer;
21
use Herrera\Box\Compactor\Php as LegacyPhp;
22
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
23
use InvalidArgumentException;
24
use KevinGH\Box\Compactor\Php;
25
use KevinGH\Box\Compactor\PhpScoper;
26
use KevinGH\Box\Composer\ComposerConfiguration;
27
use Phar;
28
use RuntimeException;
29
use SplFileInfo;
30
use stdClass;
31
use Symfony\Component\Finder\Finder;
32
use Symfony\Component\Process\Process;
33
use function Humbug\PhpScoper\create_scoper;
34
use function iter\chain;
35
use function KevinGH\Box\FileSystem\canonicalize;
36
use function KevinGH\Box\FileSystem\file_contents;
37
use function KevinGH\Box\FileSystem\longest_common_base_path;
38
use function KevinGH\Box\FileSystem\make_path_absolute;
39
use function KevinGH\Box\FileSystem\make_path_relative;
40
41
final class Configuration
42
{
43
    private const DEFAULT_ALIAS = 'default.phar';
44
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
45
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
46
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
47
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
48
    private const DEFAULT_BANNER = <<<'BANNER'
49
Generated by Humbug Box.
50
51
@link https://github.com/humbug/box
52
BANNER;
53
    private const FILES_SETTINGS = [
54
        'files',
55
        'files-bin',
56
        'directories',
57
        'directories-bin',
58
        'finder',
59
        'finder-bin',
60
    ];
61
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
62
63
    private $fileMode;
64
    private $alias;
65
    private $basePath;
66
    private $files;
67
    private $binaryFiles;
68
    private $compactors;
69
    private $compressionAlgorithm;
70
    private $mainScriptPath;
71
    private $mainScriptContents;
72
    private $map;
73
    private $fileMapper;
74
    private $metadata;
75
    private $tmpOutputPath;
76
    private $outputPath;
77
    private $privateKeyPassphrase;
78
    private $privateKeyPath;
79
    private $isPrivateKeyPrompt;
80
    private $processedReplacements;
81
    private $shebang;
82
    private $signingAlgorithm;
83
    private $stubBannerContents;
84
    private $stubBannerPath;
85
    private $stubPath;
86
    private $isInterceptFileFuncs;
87
    private $isStubGenerated;
88
89
    /**
90
     * @param null|string   $file
91
     * @param null|string   $alias
92
     * @param string        $basePath              Utility to private the base path used and be able to retrieve a path relative to it (the base path)
93
     * @param SplFileInfo[] $files                 List of files
94
     * @param SplFileInfo[] $binaryFiles           List of binary files
95
     * @param Compactor[]   $compactors            List of file contents compactors
96
     * @param null|int      $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
97
     * @param null|int      $fileMode              File mode in octal form
98
     * @param string        $mainScriptPath        The main script file path
99
     * @param string        $mainScriptContents    The processed content of the main script file
100
     * @param MapFile       $fileMapper            Utility to map the files from outside and inside the PHAR
101
     * @param mixed         $metadata              The PHAR Metadata
102
     * @param string        $tmpOutputPath
103
     * @param string        $outputPath
104
     * @param null|string   $privateKeyPassphrase
105
     * @param null|string   $privateKeyPath
106
     * @param bool          $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
107
     * @param array         $processedReplacements The processed list of replacement placeholders and their values
108
     * @param null|string   $shebang               The shebang line
109
     * @param int           $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
110
     * @param null|string   $stubBannerContents    The stub banner comment
111
     * @param null|string   $stubBannerPath        The path to the stub banner comment file
112
     * @param null|string   $stubPath              The PHAR stub file path
113
     * @param bool          $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
114
     * @param bool          $isStubGenerated       Wether or not if the PHAR stub should be generated
115
     */
116
    private function __construct(
117
        ?string $file,
118
        ?string $alias,
119
        string $basePath,
120
        array $files,
121
        array $binaryFiles,
122
        array $compactors,
123
        ?int $compressionAlgorithm,
124
        ?int $fileMode,
125
        string $mainScriptPath,
126
        string $mainScriptContents,
127
        MapFile $fileMapper,
128
        $metadata,
129
        string $tmpOutputPath,
130
        string $outputPath,
131
        ?string $privateKeyPassphrase,
132
        ?string $privateKeyPath,
133
        bool $isPrivateKeyPrompt,
134
        array $processedReplacements,
135
        ?string $shebang,
136
        int $signingAlgorithm,
137
        ?string $stubBannerContents,
138
        ?string $stubBannerPath,
139
        ?string $stubPath,
140
        bool $isInterceptFileFuncs,
141
        bool $isStubGenerated
142
    ) {
143
        Assertion::nullOrInArray(
144
            $compressionAlgorithm,
145
            get_phar_compression_algorithms(),
146
            sprintf(
147
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
148
                implode('", "', array_keys(get_phar_compression_algorithms()))
149
            )
150
        );
151
152
        $this->alias = $alias;
153
        $this->basePath = $basePath;
154
        $this->files = $files;
155
        $this->binaryFiles = $binaryFiles;
156
        $this->compactors = $compactors;
157
        $this->compressionAlgorithm = $compressionAlgorithm;
158
        $this->fileMode = $fileMode;
159
        $this->mainScriptPath = $mainScriptPath;
160
        $this->mainScriptContents = $mainScriptContents;
161
        $this->fileMapper = $fileMapper;
162
        $this->metadata = $metadata;
163
        $this->tmpOutputPath = $tmpOutputPath;
164
        $this->outputPath = $outputPath;
165
        $this->privateKeyPassphrase = $privateKeyPassphrase;
166
        $this->privateKeyPath = $privateKeyPath;
167
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
168
        $this->processedReplacements = $processedReplacements;
169
        $this->shebang = $shebang;
170
        $this->signingAlgorithm = $signingAlgorithm;
171
        $this->stubBannerContents = $stubBannerContents;
172
        $this->stubBannerPath = $stubBannerPath;
173
        $this->stubPath = $stubPath;
174
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
175
        $this->isStubGenerated = $isStubGenerated;
176
    }
177
178
    public static function create(?string $file, stdClass $raw): self
179
    {
180
        $alias = self::retrieveAlias($raw);
181
182
        $basePath = self::retrieveBasePath($file, $raw);
183
184
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
185
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
186
187
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath);
188
189
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
190
191
        if (self::shouldRetrieveAllFiles($file, $raw)) {
192
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $devPackages);
193
            $binaryFilesAggregate = [];
194
        } else {
195
            $files = self::retrieveFiles($raw, 'files', $basePath);
196
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
197
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
198
199
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
200
201
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
202
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
203
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
204
205
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
206
        }
207
208
        $compactors = self::retrieveCompactors($raw, $basePath);
209
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
210
211
        $fileMode = self::retrieveFileMode($raw);
212
213
        $map = self::retrieveMap($raw);
214
        $fileMapper = new MapFile($map);
215
216
        $metadata = self::retrieveMetadata($raw);
217
218
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath);
219
220
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
221
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
222
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
223
224
        $replacements = self::retrieveReplacements($raw);
225
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
226
227
        $shebang = self::retrieveShebang($raw);
228
229
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
230
231
        $stubBannerContents = self::retrieveStubBannerContents($raw);
232
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
233
234
        if (null !== $stubBannerPath) {
235
            $stubBannerContents = file_contents($stubBannerPath);
236
        }
237
238
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
239
240
        $stubPath = self::retrieveStubPath($raw, $basePath);
241
242
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
243
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
244
245
        return new self(
246
            $file,
247
            $alias,
248
            $basePath,
249
            $filesAggregate,
250
            $binaryFilesAggregate,
251
            $compactors,
252
            $compressionAlgorithm,
253
            $fileMode,
254
            $mainScriptPath,
255
            $mainScriptContents,
256
            $fileMapper,
257
            $metadata,
258
            $tmpOutputPath,
259
            $outputPath,
260
            $privateKeyPassphrase,
261
            $privateKeyPath,
262
            $isPrivateKeyPrompt,
263
            $processedReplacements,
264
            $shebang,
265
            $signingAlgorithm,
266
            $stubBannerContents,
267
            $stubBannerPath,
268
            $stubPath,
269
            $isInterceptFileFuncs,
270
            $isStubGenerated
271
        );
272
    }
273
274
    public function getAlias(): ?string
275
    {
276
        return $this->alias;
277
    }
278
279
    public function getBasePath(): string
280
    {
281
        return $this->basePath;
282
    }
283
284
    /**
285
     * @return string[]
286
     */
287
    public function getFiles(): array
288
    {
289
        return $this->files;
290
    }
291
292
    /**
293
     * @return string[]
294
     */
295
    public function getBinaryFiles(): array
296
    {
297
        return $this->binaryFiles;
298
    }
299
300
    /**
301
     * @return Compactor[] the list of compactors
302
     */
303
    public function getCompactors(): array
304
    {
305
        return $this->compactors;
306
    }
307
308
    public function getCompressionAlgorithm(): ?int
309
    {
310
        return $this->compressionAlgorithm;
311
    }
312
313
    public function getFileMode(): ?int
314
    {
315
        return $this->fileMode;
316
    }
317
318
    public function getMainScriptPath(): string
319
    {
320
        return $this->mainScriptPath;
321
    }
322
323
    public function getMainScriptContents(): string
324
    {
325
        return $this->mainScriptContents;
326
    }
327
328
    public function getTmpOutputPath(): string
329
    {
330
        return $this->tmpOutputPath;
331
    }
332
333
    public function getOutputPath(): string
334
    {
335
        return $this->outputPath;
336
    }
337
338
    /**
339
     * @return string[]
340
     */
341
    public function getMap(): array
342
    {
343
        return $this->fileMapper->getMap();
344
    }
345
346
    public function getFileMapper(): MapFile
347
    {
348
        return $this->fileMapper;
349
    }
350
351
    /**
352
     * @return mixed
353
     */
354
    public function getMetadata()
355
    {
356
        return $this->metadata;
357
    }
358
359
    public function getPrivateKeyPassphrase(): ?string
360
    {
361
        return $this->privateKeyPassphrase;
362
    }
363
364
    public function getPrivateKeyPath(): ?string
365
    {
366
        return $this->privateKeyPath;
367
    }
368
369
    public function isPrivateKeyPrompt(): bool
370
    {
371
        return $this->isPrivateKeyPrompt;
372
    }
373
374
    public function getProcessedReplacements(): array
375
    {
376
        return $this->processedReplacements;
377
    }
378
379
    public function getShebang(): ?string
380
    {
381
        return $this->shebang;
382
    }
383
384
    public function getSigningAlgorithm(): int
385
    {
386
        return $this->signingAlgorithm;
387
    }
388
389
    public function getStubBannerContents(): ?string
390
    {
391
        return $this->stubBannerContents;
392
    }
393
394
    public function getStubBannerPath(): ?string
395
    {
396
        return $this->stubBannerPath;
397
    }
398
399
    public function getStubPath(): ?string
400
    {
401
        return $this->stubPath;
402
    }
403
404
    public function isInterceptFileFuncs(): bool
405
    {
406
        return $this->isInterceptFileFuncs;
407
    }
408
409
    public function isStubGenerated(): bool
410
    {
411
        return $this->isStubGenerated;
412
    }
413
414
    private static function retrieveAlias(stdClass $raw): ?string
415
    {
416
        if (false === isset($raw->alias)) {
417
            return null;
418
        }
419
420
        $alias = trim($raw->alias);
421
422
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
423
424
        return $alias;
425
    }
426
427
    private static function retrieveBasePath(?string $file, stdClass $raw): string
428
    {
429
        if (null === $file) {
430
            return getcwd();
431
        }
432
433
        if (false === isset($raw->{'base-path'})) {
434
            return realpath(dirname($file));
435
        }
436
437
        $basePath = trim($raw->{'base-path'});
438
439
        Assertion::directory(
440
            $basePath,
441
            'The base path "%s" is not a directory or does not exist.'
442
        );
443
444
        return realpath($basePath);
445
    }
446
447
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
448
    {
449
        if (null === $file) {
450
            return true;
451
        }
452
453
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
454
        $rawConfig = (array) $raw;
455
456
        foreach (self::FILES_SETTINGS as $key) {
457
            if (array_key_exists($key, $rawConfig)) {
458
                return false;
459
            }
460
        }
461
462
        return true;
463
    }
464
465
    /**
466
     * @param stdClass $raw
467
     * @param string   $basePath
468
     *
469
     * @return Closure
470
     */
471
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
472
    {
473
        $blacklist = self::retrieveBlacklist($raw, $basePath);
474
475
        return function (SplFileInfo $file) use ($blacklist): ?bool {
476
            if (in_array($file->getRealPath(), $blacklist, true)) {
477
                return false;
478
            }
479
480
            return null;
481
        };
482
    }
483
484
    /**
485
     * @param stdClass $raw
486
     * @param string   $basePath
487
     *
488
     * @return string[]
489
     */
490
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
491
    {
492
        if (false === isset($raw->blacklist)) {
493
            return [];
494
        }
495
496
        $blacklist = $raw->blacklist;
497
498
        $normalizePath = function ($file) use ($basePath): string {
499
            return self::normalizePath($file, $basePath);
500
        };
501
502
        return array_unique(array_map($normalizePath, $blacklist));
503
    }
504
505
    /**
506
     * @param stdClass $raw
507
     * @param string   $key      Config property name
508
     * @param string   $basePath
509
     *
510
     * @return SplFileInfo[]
511
     */
512
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
513
    {
514
        if (false === isset($raw->{$key})) {
515
            return [];
516
        }
517
518
        $files = (array) $raw->{$key};
519
520
        Assertion::allString($files);
521
522
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
523
            $file = self::normalizePath($file, $basePath);
524
525
            if (is_link($file)) {
526
                // TODO: add this to baberlei/assert
527
                throw new InvalidArgumentException(
528
                    sprintf(
529
                        'Cannot add the link "%s": links are not supported.',
530
                        $file
531
                    )
532
                );
533
            }
534
535
            Assertion::file(
536
                $file,
537
                sprintf(
538
                    '"%s" must contain a list of existing files. Could not find "%%s".',
539
                    $key
540
                )
541
            );
542
543
            return new SplFileInfo($file);
544
        };
545
546
        return array_map($normalizePath, $files);
547
    }
548
549
    /**
550
     * @param stdClass $raw
551
     * @param string   $key             Config property name
552
     * @param string   $basePath
553
     * @param Closure  $blacklistFilter
554
     *
555
     * @return iterable|SplFileInfo[]
556
     */
557
    private static function retrieveDirectories(
558
        stdClass $raw,
559
        string $key,
560
        string $basePath,
561
        Closure $blacklistFilter
562
    ): iterable {
563
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
564
565
        if ([] !== $directories) {
566
            return Finder::create()
567
                ->files()
568
                ->filter($blacklistFilter)
569
                ->ignoreVCS(true)
570
                ->in($directories)
571
            ;
572
        }
573
574
        return [];
575
    }
576
577
    /**
578
     * @param stdClass $raw
579
     * @param string   $basePath
580
     * @param Closure  $blacklistFilter
581
     * @param string[] $devPackages
582
     *
583
     * @return iterable[]|SplFileInfo[][]
584
     */
585
    private static function retrieveFilesFromFinders(
586
        stdClass $raw,
587
        string $key,
588
        string $basePath,
589
        Closure $blacklistFilter,
590
        array $devPackages
591
    ): array {
592
        if (isset($raw->{$key})) {
593
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
594
        }
595
596
        return [];
597
    }
598
599
    /**
600
     * @param array    $findersConfig
601
     * @param string   $basePath
602
     * @param Closure  $blacklistFilter
603
     * @param string[] $devPackages
604
     *
605
     * @return Finder[]|SplFileInfo[][]
606
     */
607
    private static function processFinders(
608
        array $findersConfig,
609
        string $basePath,
610
        Closure $blacklistFilter,
611
        array $devPackages
612
    ): array {
613
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
614
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
615
        };
616
617
        return array_map($processFinderConfig, $findersConfig);
618
    }
619
620
    /**
621
     * @param stdClass $config
622
     * @param string   $basePath
623
     * @param Closure  $blacklistFilter
624
     * @param string[] $devPackages
625
     *
626
     * @return Finder|SplFileInfo[]
627
     */
628
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter, array $devPackages): Finder
629
    {
630
        $finder = Finder::create()
631
            ->files()
632
            ->filter($blacklistFilter)
633
            ->filter(
634
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
635
                    foreach ($devPackages as $devPackage) {
636
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
637
                            // File belongs to the dev package
638
                            return false;
639
                        }
640
                    }
641
642
                    return true;
643
                }
644
            )
645
            ->ignoreVCS(true)
646
        ;
647
648
        $normalizedConfig = (function (array $config, Finder $finder): array {
649
            $normalizedConfig = [];
650
651
            foreach ($config as $method => $arguments) {
652
                $method = trim($method);
653
                $arguments = (array) $arguments;
654
655
                Assertion::methodExists(
656
                    $method,
657
                    $finder,
658
                    'The method "Finder::%s" does not exist.'
659
                );
660
661
                $normalizedConfig[$method] = $arguments;
662
            }
663
664
            krsort($normalizedConfig);
665
666
            return $normalizedConfig;
667
        })((array) $config, $finder);
668
669
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
670
            $directory = self::normalizePath($directory, $basePath);
671
672
            if (is_link($directory)) {
673
                // TODO: add this to baberlei/assert
674
                throw new InvalidArgumentException(
675
                    sprintf(
676
                        'Cannot append the link "%s" to the Finder: links are not supported.',
677
                        $directory
678
                    )
679
                );
680
            }
681
682
            Assertion::directory($directory);
683
684
            return $directory;
685
        };
686
687
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
688
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
689
690
            if (is_link($fileOrDirectory)) {
691
                // TODO: add this to baberlei/assert
692
                throw new InvalidArgumentException(
693
                    sprintf(
694
                        'Cannot append the link "%s" to the Finder: links are not supported.',
695
                        $fileOrDirectory
696
                    )
697
                );
698
            }
699
700
            // TODO: add this to baberlei/assert
701
            if (false === file_exists($fileOrDirectory)) {
702
                throw new InvalidArgumentException(
703
                    sprintf(
704
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
705
                        $fileOrDirectory
706
                    )
707
                );
708
            }
709
710
            // TODO: add fileExists (as file or directory) to Assert
711
            if (false === is_file($fileOrDirectory)) {
712
                Assertion::directory($fileOrDirectory);
713
            } else {
714
                Assertion::file($fileOrDirectory);
715
            }
716
        };
717
718
        foreach ($normalizedConfig as $method => $arguments) {
719
            if ('in' === $method) {
720
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
721
            }
722
723
            if ('exclude' === $method) {
724
                $arguments = array_unique(array_map('trim', $arguments));
725
            }
726
727
            if ('append' === $method) {
728
                array_walk($arguments, $normalizeFileOrDirectory);
729
730
                $arguments = [$arguments];
731
            }
732
733
            foreach ($arguments as $argument) {
734
                $finder->$method($argument);
735
            }
736
        }
737
738
        return $finder;
739
    }
740
741
    /**
742
     * @param string   $basePath
743
     * @param string   $mainScriptPath
744
     * @param Closure  $blacklistFilter
745
     * @param string[] $devPackages
746
     *
747
     * @return SplFileInfo[]
748
     */
749
    private static function retrieveAllFiles(
750
        string $basePath,
751
        string $mainScriptPath,
752
        Closure $blacklistFilter,
753
        array $devPackages
754
    ): array {
755
        $relativeDevPackages = array_map(
756
            function (string $packagePath) use ($basePath): string {
757
                return make_path_relative($packagePath, $basePath);
758
            },
759
            $devPackages
760
        );
761
762
        $finder = Finder::create()
763
            ->files()
764
            ->in($basePath)
765
            ->notPath(make_path_relative($mainScriptPath, $basePath))
766
            ->filter($blacklistFilter)
767
            ->exclude($relativeDevPackages)
768
            ->ignoreVCS(true)
769
        ;
770
771
        return array_filter(
772
            array_unique(
773
                array_map(
774
                    function (SplFileInfo $fileInfo): ?string {
775
                        if (is_link((string) $fileInfo)) {
776
                            return null;
777
                        }
778
779
                        return false !== $fileInfo->getRealPath() ? $fileInfo->getRealPath() : null;
780
                    },
781
                    iterator_to_array(
782
                        $finder
783
                    )
784
                )
785
            )
786
        );
787
    }
788
789
    /**
790
     * @param stdClass $raw
791
     * @param string   $key      Config property name
792
     * @param string   $basePath
793
     *
794
     * @return string[]
795
     */
796
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
797
    {
798
        if (false === isset($raw->{$key})) {
799
            return [];
800
        }
801
802
        $directories = $raw->{$key};
803
804
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
805
            $directory = self::normalizePath($directory, $basePath);
806
807
            if (is_link($directory)) {
808
                // TODO: add this to baberlei/assert
809
                throw new InvalidArgumentException(
810
                    sprintf(
811
                        'Cannot add the link "%s": links are not supported.',
812
                        $directory
813
                    )
814
                );
815
            }
816
817
            Assertion::directory(
818
                $directory,
819
                sprintf(
820
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
821
                    $key
822
                )
823
            );
824
825
            return $directory;
826
        };
827
828
        return array_map($normalizeDirectory, $directories);
829
    }
830
831
    private static function normalizePath(string $file, string $basePath): string
832
    {
833
        return make_path_absolute(trim($file), $basePath);
834
    }
835
836
    /**
837
     * @return Compactor[]
838
     */
839
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
840
    {
841
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
842
        if (false === isset($raw->compactors)) {
843
            return [];
844
        }
845
846
        $compactorClasses = array_unique((array) $raw->compactors);
847
848
        return array_map(
849
            function (string $class) use ($raw, $basePath): Compactor {
850
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
851
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
852
853
                if (Php::class === $class || LegacyPhp::class === $class) {
854
                    return self::createPhpCompactor($raw);
855
                }
856
857
                if (PhpScoper::class === $class) {
858
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
859
860
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
861
                }
862
863
                return new $class();
864
            },
865
            $compactorClasses
866
        );
867
    }
868
869
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
870
    {
871
        // TODO: if in dev mode (when added), do not comment about the compression.
872
        // If not, add a warning to notify the user if no compression algorithm is used
873
        // provided the PHAR is not configured for web purposes.
874
        // If configured for the web, add a warning when a compression algorithm is used
875
        // as this can result in an overhead. Add a doc link explaining this.
876
        //
877
        // Unlike the doc: do not accept integers and document this BC break.
878
        if (false === isset($raw->compression)) {
879
            return null;
880
        }
881
882
        if (false === is_string($raw->compression)) {
883
            Assertion::integer(
884
                $raw->compression,
885
                'Expected compression to be an algorithm name, found %s instead.'
886
            );
887
888
            return $raw->compression;
889
        }
890
891
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
892
893
        Assertion::inArray(
894
            $raw->compression,
895
            $knownAlgorithmNames,
896
            sprintf(
897
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
898
                implode('", "', $knownAlgorithmNames)
899
            )
900
        );
901
902
        $value = get_phar_compression_algorithms()[$raw->compression];
903
904
        // Phar::NONE is not valid for compressFiles()
905
        if (Phar::NONE === $value) {
906
            return null;
907
        }
908
909
        return $value;
910
    }
911
912
    private static function retrieveFileMode(stdClass $raw): ?int
913
    {
914
        if (isset($raw->chmod)) {
915
            return intval($raw->chmod, 8);
916
        }
917
918
        return null;
919
    }
920
921
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): string
922
    {
923
        $main = isset($raw->main) ? $raw->main : self::DEFAULT_MAIN_SCRIPT;
924
925
        return self::normalizePath($main, $basePath);
926
    }
927
928
    private static function retrieveMainScriptContents(string $mainScriptPath): string
929
    {
930
        $contents = file_contents($mainScriptPath);
931
932
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
933
        // PHAR entry point file.
934
        return preg_replace('/^#!.*\s*/', '', $contents);
935
    }
936
937
    /**
938
     * @return string[][]
939
     */
940
    private static function retrieveMap(stdClass $raw): array
941
    {
942
        if (false === isset($raw->map)) {
943
            return [];
944
        }
945
946
        $map = [];
947
948
        foreach ((array) $raw->map as $item) {
949
            $processed = [];
950
951
            foreach ($item as $match => $replace) {
952
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
953
            }
954
955
            if (isset($processed['_empty_'])) {
956
                $processed[''] = $processed['_empty_'];
957
958
                unset($processed['_empty_']);
959
            }
960
961
            $map[] = $processed;
962
        }
963
964
        return $map;
965
    }
966
967
    /**
968
     * @return mixed
969
     */
970
    private static function retrieveMetadata(stdClass $raw)
971
    {
972
        // TODO: the doc currently say this can be any value; check if true
973
        // and if not add checks accordingly
974
        //
975
        // Also review the doc as I don't find it very helpful...
976
        if (isset($raw->metadata)) {
977
            if (is_object($raw->metadata)) {
978
                return (array) $raw->metadata;
979
            }
980
981
            return $raw->metadata;
982
        }
983
984
        return null;
985
    }
986
987
    /**
988
     * @return string[] The first element is the temporary output path and the second the real one
989
     */
990
    private static function retrieveOutputPath(stdClass $raw, string $basePath): array
991
    {
992
        if (isset($raw->output)) {
993
            $path = $raw->output;
994
        } else {
995
            $path = self::DEFAULT_ALIAS;
996
        }
997
998
        $tmp = $real = self::normalizePath($path, $basePath);
999
1000
        if ('.phar' !== substr($real, -5)) {
1001
            $tmp .= '.phar';
1002
        }
1003
1004
        return [$tmp, $real];
1005
    }
1006
1007
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1008
    {
1009
        // TODO: add check to not allow this setting without the private key path
1010
        if (isset($raw->{'key-pass'})
1011
            && is_string($raw->{'key-pass'})
1012
        ) {
1013
            return $raw->{'key-pass'};
1014
        }
1015
1016
        return null;
1017
    }
1018
1019
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1020
    {
1021
        // TODO: If passed need to check its existence
1022
        // Also need
1023
1024
        if (isset($raw->key)) {
1025
            return $raw->key;
1026
        }
1027
1028
        return null;
1029
    }
1030
1031
    private static function retrieveReplacements(stdClass $raw): array
1032
    {
1033
        // TODO: add exmample in the doc
1034
        // Add checks against the values
1035
        if (isset($raw->replacements)) {
1036
            return (array) $raw->replacements;
1037
        }
1038
1039
        return [];
1040
    }
1041
1042
    private static function retrieveProcessedReplacements(
1043
        array $replacements,
1044
        stdClass $raw,
1045
        ?string $file
1046
    ): array {
1047
        if (null === $file) {
1048
            return [];
1049
        }
1050
1051
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1052
            $replacements[$git] = self::retrieveGitHash($file);
1053
        }
1054
1055
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1056
            $replacements[$git] = self::retrieveGitHash($file, true);
1057
        }
1058
1059
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1060
            $replacements[$git] = self::retrieveGitTag($file);
1061
        }
1062
1063
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1064
            $replacements[$git] = self::retrieveGitVersion($file);
1065
        }
1066
1067
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1068
            $replacements[$date] = self::retrieveDatetimeNow(
1069
                self::retrieveDatetimeFormat($raw)
1070
            );
1071
        }
1072
1073
        $sigil = self::retrieveReplacementSigil($raw);
1074
1075
        foreach ($replacements as $key => $value) {
1076
            unset($replacements[$key]);
1077
            $replacements["$sigil$key$sigil"] = $value;
1078
        }
1079
1080
        return $replacements;
1081
    }
1082
1083
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1084
    {
1085
        if (isset($raw->{'git-commit'})) {
1086
            return $raw->{'git-commit'};
1087
        }
1088
1089
        return null;
1090
    }
1091
1092
    /**
1093
     * @param string $file
1094
     * @param bool   $short Use the short version
1095
     *
1096
     * @return string the commit hash
1097
     */
1098
    private static function retrieveGitHash(string $file, bool $short = false): string
1099
    {
1100
        return self::runGitCommand(
1101
            sprintf(
1102
                'git log --pretty="%s" -n1 HEAD',
1103
                $short ? '%h' : '%H'
1104
            ),
1105
            $file
1106
        );
1107
    }
1108
1109
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1110
    {
1111
        if (isset($raw->{'git-commit-short'})) {
1112
            return $raw->{'git-commit-short'};
1113
        }
1114
1115
        return null;
1116
    }
1117
1118
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1119
    {
1120
        if (isset($raw->{'git-tag'})) {
1121
            return $raw->{'git-tag'};
1122
        }
1123
1124
        return null;
1125
    }
1126
1127
    private static function retrieveGitTag(string $file): ?string
1128
    {
1129
        return self::runGitCommand('git describe --tags HEAD', $file);
1130
    }
1131
1132
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1133
    {
1134
        if (isset($raw->{'git-version'})) {
1135
            return $raw->{'git-version'};
1136
        }
1137
1138
        return null;
1139
    }
1140
1141
    private static function retrieveGitVersion(string $file): ?string
1142
    {
1143
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1144
        // to avoid messing around with that
1145
1146
        try {
1147
            return self::retrieveGitTag($file);
1148
        } catch (RuntimeException $exception) {
1149
            try {
1150
                return self::retrieveGitHash($file, true);
1151
            } catch (RuntimeException $exception) {
1152
                throw new RuntimeException(
1153
                    sprintf(
1154
                        'The tag or commit hash could not be retrieved from "%s": %s',
1155
                        dirname($file),
1156
                        $exception->getMessage()
1157
                    ),
1158
                    0,
1159
                    $exception
1160
                );
1161
            }
1162
        }
1163
    }
1164
1165
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1166
    {
1167
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1168
        // Also make sure the documentation is up to date after.
1169
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1170
        // be better to have only one element IMO like:
1171
        //
1172
        // "datetime": {
1173
        //   "value": "val",
1174
        //   "format": "Y-m-d"
1175
        // }
1176
        //
1177
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1178
        // if the datetime format is the default one it's ok; but in any case the format should not
1179
        // be added without the datetime value...
1180
1181
        if (isset($raw->{'datetime'})) {
1182
            return $raw->{'datetime'};
1183
        }
1184
1185
        return null;
1186
    }
1187
1188
    private static function retrieveDatetimeNow(string $format)
1189
    {
1190
        $now = new DateTimeImmutable('now');
1191
1192
        $datetime = $now->format($format);
1193
1194
        if (!$datetime) {
1195
            throw new InvalidArgumentException(
1196
                sprintf(
1197
                    '""%s" is not a valid PHP date format',
1198
                    $format
1199
                )
1200
            );
1201
        }
1202
1203
        return $datetime;
1204
    }
1205
1206
    private static function retrieveDatetimeFormat(stdClass $raw): string
1207
    {
1208
        if (isset($raw->{'datetime_format'})) {
1209
            return $raw->{'datetime_format'};
1210
        }
1211
1212
        return self::DEFAULT_DATETIME_FORMAT;
1213
    }
1214
1215
    private static function retrieveReplacementSigil(stdClass $raw)
1216
    {
1217
        if (isset($raw->{'replacement-sigil'})) {
1218
            return $raw->{'replacement-sigil'};
1219
        }
1220
1221
        return self::DEFAULT_REPLACEMENT_SIGIL;
1222
    }
1223
1224
    private static function retrieveShebang(stdClass $raw): ?string
1225
    {
1226
        if (false === array_key_exists('shebang', (array) $raw)) {
1227
            return self::DEFAULT_SHEBANG;
1228
        }
1229
1230
        if (null === $raw->shebang) {
1231
            return null;
1232
        }
1233
1234
        $shebang = trim($raw->shebang);
1235
1236
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1237
        Assertion::true(
1238
            '#!' === substr($shebang, 0, 2),
1239
            sprintf(
1240
                'The shebang line must start with "#!". Got "%s" instead',
1241
                $shebang
1242
            )
1243
        );
1244
1245
        return $shebang;
1246
    }
1247
1248
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1249
    {
1250
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1251
        // TODO: trigger a warning if the signing algorithm used is weak
1252
        // TODO: no longer accept strings & document BC break
1253
        if (false === isset($raw->algorithm)) {
1254
            return Phar::SHA1;
1255
        }
1256
1257
        if (is_string($raw->algorithm)) {
1258
            if (false === defined('Phar::'.$raw->algorithm)) {
1259
                throw new InvalidArgumentException(
1260
                    sprintf(
1261
                        'The signing algorithm "%s" is not supported.',
1262
                        $raw->algorithm
1263
                    )
1264
                );
1265
            }
1266
1267
            return constant('Phar::'.$raw->algorithm);
1268
        }
1269
1270
        return $raw->algorithm;
1271
    }
1272
1273
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1274
    {
1275
        if (false === array_key_exists('banner', (array) $raw)) {
1276
            return self::DEFAULT_BANNER;
1277
        }
1278
1279
        if (null === $raw->banner) {
1280
            return null;
1281
        }
1282
1283
        $banner = $raw->banner;
1284
1285
        if (is_array($banner)) {
1286
            $banner = implode("\n", $banner);
1287
        }
1288
1289
        return $banner;
1290
    }
1291
1292
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1293
    {
1294
        if (false === isset($raw->{'banner-file'})) {
1295
            return null;
1296
        }
1297
1298
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1299
1300
        Assertion::file($bannerFile);
1301
1302
        return $bannerFile;
1303
    }
1304
1305
    private static function normalizeStubBannerContents(?string $contents): ?string
1306
    {
1307
        if (null === $contents) {
1308
            return null;
1309
        }
1310
1311
        $banner = explode("\n", $contents);
1312
        $banner = array_map('trim', $banner);
1313
1314
        return implode("\n", $banner);
1315
    }
1316
1317
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1318
    {
1319
        if (isset($raw->stub) && is_string($raw->stub)) {
1320
            $stubPath = make_path_absolute($raw->stub, $basePath);
1321
1322
            Assertion::file($stubPath);
1323
1324
            return $stubPath;
1325
        }
1326
1327
        return null;
1328
    }
1329
1330
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1331
    {
1332
        if (isset($raw->intercept)) {
1333
            return $raw->intercept;
1334
        }
1335
1336
        return false;
1337
    }
1338
1339
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1340
    {
1341
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1342
    }
1343
1344
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1345
    {
1346
        return isset($raw->stub) && (true === $raw->stub);
1347
    }
1348
1349
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1350
    {
1351
        if (!isset($raw->{'php-scoper'})) {
1352
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1353
1354
            return file_exists($configFilePath)
1355
                ? PhpScoperConfiguration::load($configFilePath)
1356
                : PhpScoperConfiguration::load()
1357
             ;
1358
        }
1359
1360
        $configFile = $raw->phpScoper;
1361
1362
        Assertion::string($configFile);
1363
1364
        $configFilePath = make_path_absolute($configFile, $basePath);
1365
1366
        Assertion::file($configFilePath);
1367
        Assertion::readable($configFilePath);
1368
1369
        return PhpScoperConfiguration::load($configFilePath);
1370
    }
1371
1372
    /**
1373
     * Runs a Git command on the repository.
1374
     *
1375
     * @param string $command the command
1376
     *
1377
     * @return string the trimmed output from the command
1378
     */
1379
    private static function runGitCommand(string $command, string $file): string
1380
    {
1381
        $path = dirname($file);
1382
1383
        $process = new Process($command, $path);
1384
1385
        if (0 === $process->run()) {
1386
            return trim($process->getOutput());
1387
        }
1388
1389
        throw new RuntimeException(
1390
            sprintf(
1391
                'The tag or commit hash could not be retrieved from "%s": %s',
1392
                $path,
1393
                $process->getErrorOutput()
1394
            )
1395
        );
1396
    }
1397
1398
    private static function createPhpCompactor(stdClass $raw): Compactor
1399
    {
1400
        // TODO: false === not set; check & add test/doc
1401
        $tokenizer = new Tokenizer();
1402
1403
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1404
            $tokenizer->ignore(
1405
                (array) $raw->annotations->ignore
1406
            );
1407
        }
1408
1409
        return new Php($tokenizer);
1410
    }
1411
}
1412