Completed
Pull Request — master (#146)
by Théo
02:11
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 67
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 9.2815
c 0
b 0
f 0
cc 1
eloc 34
nc 1
nop 28

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 as PhpScoperCompactor;
26
use KevinGH\Box\Composer\ComposerConfiguration;
27
use KevinGH\Box\Json\Json;
28
use KevinGH\Box\PhpScoper\SimpleScoper;
29
use Phar;
30
use RuntimeException;
31
use Seld\JsonLint\ParsingException;
32
use SplFileInfo;
33
use stdClass;
34
use Symfony\Component\Finder\Finder;
35
use Symfony\Component\Process\Process;
36
use function array_column;
37
use function array_filter;
38
use function array_flip;
39
use function array_key_exists;
40
use function array_map;
41
use function array_merge;
42
use function array_unique;
43
use function file_exists;
44
use function Humbug\PhpScoper\create_scoper;
45
use function is_array;
46
use function is_file;
47
use function is_readable;
48
use function iter\fn\method;
49
use function iter\map;
50
use function iter\toArray;
51
use function iter\values;
52
use function KevinGH\Box\FileSystem\canonicalize;
53
use function KevinGH\Box\FileSystem\file_contents;
54
use function KevinGH\Box\FileSystem\is_absolute_path;
55
use function KevinGH\Box\FileSystem\longest_common_base_path;
56
use function KevinGH\Box\FileSystem\make_path_absolute;
57
use function KevinGH\Box\FileSystem\make_path_relative;
58
use function preg_match;
59
use function substr;
60
use function uniqid;
61
62
/**
63
 * @private
64
 */
65
final class Configuration
66
{
67
    private const DEFAULT_ALIAS = 'test.phar';
68
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
69
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
70
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
71
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
72
    private const DEFAULT_BANNER = <<<'BANNER'
73
Generated by Humbug Box.
74
75
@link https://github.com/humbug/box
76
BANNER;
77
    private const FILES_SETTINGS = [
78
        'directories',
79
        'finder',
80
    ];
81
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
82
83
    private $file;
84
    private $fileMode;
85
    private $alias;
86
    private $basePath;
87
    private $composerJson;
88
    private $composerLock;
89
    private $files;
90
    private $binaryFiles;
91
    private $compactors;
92
    private $compressionAlgorithm;
93
    private $mainScriptPath;
94
    private $mainScriptContents;
95
    private $map;
96
    private $fileMapper;
97
    private $metadata;
98
    private $tmpOutputPath;
99
    private $outputPath;
100
    private $privateKeyPassphrase;
101
    private $privateKeyPath;
102
    private $isPrivateKeyPrompt;
103
    private $processedReplacements;
104
    private $shebang;
105
    private $signingAlgorithm;
106
    private $stubBannerContents;
107
    private $stubBannerPath;
108
    private $stubPath;
109
    private $isInterceptFileFuncs;
110
    private $isStubGenerated;
111
    private $checkRequirements;
112
113
    /**
114
     * @param null|string     $file
115
     * @param null|string     $alias
116
     * @param string          $basePath              Utility to private the base path used and be able to retrieve a path relative to it (the base path)
117
     * @param null[]|string[] $composerJson
118
     * @param null[]|string[] $composerLock
119
     * @param SplFileInfo[]   $files                 List of files
120
     * @param SplFileInfo[]   $binaryFiles           List of binary files
121
     * @param Compactor[]     $compactors            List of file contents compactors
122
     * @param null|int        $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
123
     * @param null|int        $fileMode              File mode in octal form
124
     * @param string          $mainScriptPath        The main script file path
125
     * @param string          $mainScriptContents    The processed content of the main script file
126
     * @param MapFile         $fileMapper            Utility to map the files from outside and inside the PHAR
127
     * @param mixed           $metadata              The PHAR Metadata
128
     * @param string          $tmpOutputPath
129
     * @param string          $outputPath
130
     * @param null|string     $privateKeyPassphrase
131
     * @param null|string     $privateKeyPath
132
     * @param bool            $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
133
     * @param array           $processedReplacements The processed list of replacement placeholders and their values
134
     * @param null|string     $shebang               The shebang line
135
     * @param int             $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
136
     * @param null|string     $stubBannerContents    The stub banner comment
137
     * @param null|string     $stubBannerPath        The path to the stub banner comment file
138
     * @param null|string     $stubPath              The PHAR stub file path
139
     * @param bool            $isInterceptFileFuncs  Whether or not Phar::interceptFileFuncs() should be used
140
     * @param bool            $isStubGenerated       Whether or not if the PHAR stub should be generated
141
     * @param bool            $checkRequirements     Whether the PHAR will check the application requirements before running
142
     */
143
    private function __construct(
144
        ?string $file,
145
        string $alias,
146
        string $basePath,
147
        array $composerJson,
148
        array $composerLock,
149
        array $files,
150
        array $binaryFiles,
151
        array $compactors,
152
        ?int $compressionAlgorithm,
153
        ?int $fileMode,
154
        string $mainScriptPath,
155
        string $mainScriptContents,
156
        MapFile $fileMapper,
157
        $metadata,
158
        string $tmpOutputPath,
159
        string $outputPath,
160
        ?string $privateKeyPassphrase,
161
        ?string $privateKeyPath,
162
        bool $isPrivateKeyPrompt,
163
        array $processedReplacements,
164
        ?string $shebang,
165
        int $signingAlgorithm,
166
        ?string $stubBannerContents,
167
        ?string $stubBannerPath,
168
        ?string $stubPath,
169
        bool $isInterceptFileFuncs,
170
        bool $isStubGenerated,
171
        bool $checkRequirements
172
    ) {
173
        Assertion::nullOrInArray(
174
            $compressionAlgorithm,
175
            get_phar_compression_algorithms(),
176
            sprintf(
177
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
178
                implode('", "', array_keys(get_phar_compression_algorithms()))
179
            )
180
        );
181
182
        $this->file = $file;
183
        $this->alias = $alias;
184
        $this->basePath = $basePath;
185
        $this->composerJson = $composerJson;
186
        $this->composerLock = $composerLock;
187
        $this->files = $files;
188
        $this->binaryFiles = $binaryFiles;
189
        $this->compactors = $compactors;
190
        $this->compressionAlgorithm = $compressionAlgorithm;
191
        $this->fileMode = $fileMode;
192
        $this->mainScriptPath = $mainScriptPath;
193
        $this->mainScriptContents = $mainScriptContents;
194
        $this->fileMapper = $fileMapper;
195
        $this->metadata = $metadata;
196
        $this->tmpOutputPath = $tmpOutputPath;
197
        $this->outputPath = $outputPath;
198
        $this->privateKeyPassphrase = $privateKeyPassphrase;
199
        $this->privateKeyPath = $privateKeyPath;
200
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
201
        $this->processedReplacements = $processedReplacements;
202
        $this->shebang = $shebang;
203
        $this->signingAlgorithm = $signingAlgorithm;
204
        $this->stubBannerContents = $stubBannerContents;
205
        $this->stubBannerPath = $stubBannerPath;
206
        $this->stubPath = $stubPath;
207
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
208
        $this->isStubGenerated = $isStubGenerated;
209
        $this->checkRequirements = $checkRequirements;
210
    }
211
212
    public static function create(?string $file, stdClass $raw): self
213
    {
214
        $alias = self::retrieveAlias($raw);
215
216
        $basePath = self::retrieveBasePath($file, $raw);
217
218
        $composerFiles = self::retrieveComposerFiles($basePath);
219
220
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1]);
221
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
222
223
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath);
224
225
        $composerJson = $composerFiles[0];
226
        $composerLock = $composerFiles[1];
227
228
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
229
230
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter($raw, $basePath, $tmpOutputPath, $outputPath);
231
232
        $files = self::retrieveFiles($raw, 'files', $basePath, $composerFiles);
233
234
        if (self::shouldRetrieveAllFiles($file, $raw)) {
235
            [$files, $directories] = self::retrieveAllDirectoriesToInclude(
236
                $basePath,
237
                $composerJson[1],
238
                $devPackages,
239
                array_merge(
240
                    $files,
241
                    array_filter(
242
                        array_column($composerFiles, 0)
243
                    )
244
                ),
245
                $excludedPaths
246
            );
247
248
            $filesAggregate = self::retrieveAllFiles(
249
                $basePath,
250
                $files,
251
                $directories,
252
                $mainScriptPath,
253
                $blacklistFilter,
254
                $excludedPaths,
255
                $devPackages
256
            );
257
        } else {
258
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter, $excludedPaths);
259
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
260
261
            $filesAggregate = self::retrieveFilesAggregate($files, $directories, ...$filesFromFinders);
262
        }
263
264
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
265
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter, $excludedPaths);
266
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
267
268
        $binaryFilesAggregate = self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
269
270
        $compactors = self::retrieveCompactors($raw, $basePath);
271
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
272
273
        $fileMode = self::retrieveFileMode($raw);
274
275
        $map = self::retrieveMap($raw);
276
        $fileMapper = new MapFile($map);
277
278
        $metadata = self::retrieveMetadata($raw);
279
280
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
281
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
282
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
283
284
        $replacements = self::retrieveReplacements($raw);
285
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
286
287
        $shebang = self::retrieveShebang($raw);
288
289
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
290
291
        $stubBannerContents = self::retrieveStubBannerContents($raw);
292
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
293
294
        if (null !== $stubBannerPath) {
295
            $stubBannerContents = file_contents($stubBannerPath);
296
        }
297
298
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
299
300
        $stubPath = self::retrieveStubPath($raw, $basePath);
301
302
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
303
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath);
304
305
        $checkRequirements = self::retrieveCheckRequirements(
306
            $raw,
307
            null !== $composerLock[0],
308
            $isStubGenerated
309
        );
310
311
        return new self(
312
            $file,
313
            $alias,
314
            $basePath,
315
            $composerJson,
316
            $composerLock,
317
            $filesAggregate,
318
            $binaryFilesAggregate,
319
            $compactors,
320
            $compressionAlgorithm,
321
            $fileMode,
322
            $mainScriptPath,
323
            $mainScriptContents,
324
            $fileMapper,
325
            $metadata,
326
            $tmpOutputPath,
327
            $outputPath,
328
            $privateKeyPassphrase,
329
            $privateKeyPath,
330
            $isPrivateKeyPrompt,
331
            $processedReplacements,
332
            $shebang,
333
            $signingAlgorithm,
334
            $stubBannerContents,
335
            $stubBannerPath,
336
            $stubPath,
337
            $isInterceptFileFuncs,
338
            $isStubGenerated,
339
            $checkRequirements
340
        );
341
    }
342
343
    public function getFile(): ?string
344
    {
345
        return $this->file;
346
    }
347
348
    public function getAlias(): string
349
    {
350
        return $this->alias;
351
    }
352
353
    public function getBasePath(): string
354
    {
355
        return $this->basePath;
356
    }
357
358
    public function getComposerJson(): ?string
359
    {
360
        return $this->composerJson[0];
361
    }
362
363
    public function getComposerJsonDecodedContents(): ?array
364
    {
365
        return $this->composerJson[1];
366
    }
367
368
    public function getComposerLock(): ?string
369
    {
370
        return $this->composerLock[0];
371
    }
372
373
    public function getComposerLockDecodedContents(): ?array
374
    {
375
        return $this->composerLock[1];
376
    }
377
378
    /**
379
     * @return string[]
380
     */
381
    public function getFiles(): array
382
    {
383
        return $this->files;
384
    }
385
386
    /**
387
     * @return string[]
388
     */
389
    public function getBinaryFiles(): array
390
    {
391
        return $this->binaryFiles;
392
    }
393
394
    /**
395
     * @return Compactor[] the list of compactors
396
     */
397
    public function getCompactors(): array
398
    {
399
        return $this->compactors;
400
    }
401
402
    public function getCompressionAlgorithm(): ?int
403
    {
404
        return $this->compressionAlgorithm;
405
    }
406
407
    public function getFileMode(): ?int
408
    {
409
        return $this->fileMode;
410
    }
411
412
    public function getMainScriptPath(): string
413
    {
414
        return $this->mainScriptPath;
415
    }
416
417
    public function getMainScriptContents(): string
418
    {
419
        return $this->mainScriptContents;
420
    }
421
422
    public function checkRequirements(): bool
423
    {
424
        return $this->checkRequirements;
425
    }
426
427
    public function getTmpOutputPath(): string
428
    {
429
        return $this->tmpOutputPath;
430
    }
431
432
    public function getOutputPath(): string
433
    {
434
        return $this->outputPath;
435
    }
436
437
    /**
438
     * @return string[]
439
     */
440
    public function getMap(): array
441
    {
442
        return $this->fileMapper->getMap();
443
    }
444
445
    public function getFileMapper(): MapFile
446
    {
447
        return $this->fileMapper;
448
    }
449
450
    /**
451
     * @return mixed
452
     */
453
    public function getMetadata()
454
    {
455
        return $this->metadata;
456
    }
457
458
    public function getPrivateKeyPassphrase(): ?string
459
    {
460
        return $this->privateKeyPassphrase;
461
    }
462
463
    public function getPrivateKeyPath(): ?string
464
    {
465
        return $this->privateKeyPath;
466
    }
467
468
    public function isPrivateKeyPrompt(): bool
469
    {
470
        return $this->isPrivateKeyPrompt;
471
    }
472
473
    public function getProcessedReplacements(): array
474
    {
475
        return $this->processedReplacements;
476
    }
477
478
    public function getShebang(): ?string
479
    {
480
        return $this->shebang;
481
    }
482
483
    public function getSigningAlgorithm(): int
484
    {
485
        return $this->signingAlgorithm;
486
    }
487
488
    public function getStubBannerContents(): ?string
489
    {
490
        return $this->stubBannerContents;
491
    }
492
493
    public function getStubBannerPath(): ?string
494
    {
495
        return $this->stubBannerPath;
496
    }
497
498
    public function getStubPath(): ?string
499
    {
500
        return $this->stubPath;
501
    }
502
503
    public function isInterceptFileFuncs(): bool
504
    {
505
        return $this->isInterceptFileFuncs;
506
    }
507
508
    public function isStubGenerated(): bool
509
    {
510
        return $this->isStubGenerated;
511
    }
512
513
    private static function retrieveAlias(stdClass $raw): string
514
    {
515
        if (false === isset($raw->alias)) {
516
            return uniqid('box-auto-generated-alias-', false).'.phar';
517
        }
518
519
        $alias = trim($raw->alias);
520
521
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
522
523
        return $alias;
524
    }
525
526
    private static function retrieveBasePath(?string $file, stdClass $raw): string
527
    {
528
        if (null === $file) {
529
            return getcwd();
530
        }
531
532
        if (false === isset($raw->{'base-path'})) {
533
            return realpath(dirname($file));
534
        }
535
536
        $basePath = trim($raw->{'base-path'});
537
538
        Assertion::directory(
539
            $basePath,
540
            'The base path "%s" is not a directory or does not exist.'
541
        );
542
543
        return realpath($basePath);
544
    }
545
546
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
547
    {
548
        if (null === $file) {
549
            return true;
550
        }
551
552
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
553
        $rawConfig = (array) $raw;
554
555
        foreach (self::FILES_SETTINGS as $key) {
556
            if (array_key_exists($key, $rawConfig)) {
557
                return false;
558
            }
559
        }
560
561
        return true;
562
    }
563
564
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath, string ...$excludedPaths): array
565
    {
566
        $blacklist = self::retrieveBlacklist($raw, $basePath, ...$excludedPaths);
567
568
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
569
            if ($file->isLink()) {
570
                return false;
571
            }
572
573
            if (false === $file->getRealPath()) {
574
                return false;
575
            }
576
577
            if (in_array($file->getRealPath(), $blacklist, true)) {
578
                return false;
579
            }
580
581
            return null;
582
        };
583
584
        return [$blacklist, $blacklistFilter];
585
    }
586
587
    /**
588
     * @param stdClass $raw
589
     * @param string   $basePath
590
     * @param string[] $excludedPaths
591
     *
592
     * @return string[]
593
     */
594
    private static function retrieveBlacklist(stdClass $raw, string $basePath, string ...$excludedPaths): array
595
    {
596
        /** @var string[] $blacklist */
597
        $blacklist = array_merge($excludedPaths, $raw->blacklist ?? []);
598
599
        $normalizedBlacklist = [];
600
601
        foreach ($blacklist as $file) {
602
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
603
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
604
        }
605
606
        return array_unique($normalizedBlacklist);
607
    }
608
609
    /**
610
     * @return SplFileInfo[]
611
     */
612
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath, array $composerFiles = []): array
613
    {
614
        $files = [];
615
616
        if (isset($composerFiles[0][0])) {
617
            $files[] = $composerFiles[0][0];
618
        }
619
620
        if (isset($composerFiles[1][1])) {
621
            $files[] = $composerFiles[1][0];
622
        }
623
624
        if (false === isset($raw->{$key})) {
625
            return [];
626
        }
627
628
        $files = array_merge((array) $raw->{$key}, $files);
629
630
        Assertion::allString($files);
631
632
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
633
            $file = self::normalizePath($file, $basePath);
634
635
            if (is_link($file)) {
636
                // TODO: add this to baberlei/assert
637
                throw new InvalidArgumentException(
638
                    sprintf(
639
                        'Cannot add the link "%s": links are not supported.',
640
                        $file
641
                    )
642
                );
643
            }
644
645
            Assertion::file(
646
                $file,
647
                sprintf(
648
                    '"%s" must contain a list of existing files. Could not find "%%s".',
649
                    $key
650
                )
651
            );
652
653
            return new SplFileInfo($file);
654
        };
655
656
        return array_map($normalizePath, $files);
657
    }
658
659
    /**
660
     * @param stdClass $raw
661
     * @param string   $key             Config property name
662
     * @param string   $basePath
663
     * @param Closure  $blacklistFilter
664
     * @param string[] $excludedPaths
665
     *
666
     * @return iterable|SplFileInfo[]
667
     */
668
    private static function retrieveDirectories(
669
        stdClass $raw,
670
        string $key,
671
        string $basePath,
672
        Closure $blacklistFilter,
673
        array $excludedPaths
674
    ): iterable {
675
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
676
677
        if ([] !== $directories) {
678
            $finder = Finder::create()
679
                ->files()
680
                ->filter($blacklistFilter)
681
                ->ignoreVCS(true)
682
                ->in($directories)
683
            ;
684
685
            foreach ($excludedPaths as $excludedPath) {
686
                $finder->notPath($excludedPath);
687
            }
688
689
            return $finder;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
690
        }
691
692
        return [];
693
    }
694
695
    /**
696
     * @param stdClass $raw
697
     * @param string   $key
698
     * @param string   $basePath
699
     * @param Closure  $blacklistFilter
700
     * @param string[] $devPackages
701
     *
702
     * @return iterable[]|SplFileInfo[][]
703
     */
704
    private static function retrieveFilesFromFinders(
705
        stdClass $raw,
706
        string $key,
707
        string $basePath,
708
        Closure $blacklistFilter,
709
        array $devPackages
710
    ): array {
711
        if (isset($raw->{$key})) {
712
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
713
        }
714
715
        return [];
716
    }
717
718
    /**
719
     * @param iterable[]|SplFileInfo[][] $fileIterators
720
     *
721
     * @return SplFileInfo[]
722
     */
723
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
724
    {
725
        $files = [];
726
727
        foreach ($fileIterators as $fileIterator) {
728
            foreach ($fileIterator as $file) {
729
                $files[$file->getPathname()] = $file;
730
            }
731
        }
732
733
        return array_values($files);
734
    }
735
736
    /**
737
     * @param array    $findersConfig
738
     * @param string   $basePath
739
     * @param Closure  $blacklistFilter
740
     * @param string[] $devPackages
741
     *
742
     * @return Finder[]|SplFileInfo[][]
743
     */
744
    private static function processFinders(
745
        array $findersConfig,
746
        string $basePath,
747
        Closure $blacklistFilter,
748
        array $devPackages
749
    ): array {
750
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
751
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
752
        };
753
754
        return array_map($processFinderConfig, $findersConfig);
755
    }
756
757
    /**
758
     * @param stdClass $config
759
     * @param string   $basePath
760
     * @param Closure  $blacklistFilter
761
     * @param string[] $devPackages
762
     *
763
     * @return Finder|SplFileInfo[]
764
     */
765
    private static function processFinder(
766
        stdClass $config,
767
        string $basePath,
768
        Closure $blacklistFilter,
769
        array $devPackages
770
    ): Finder {
771
        $finder = Finder::create()
772
            ->files()
773
            ->filter($blacklistFilter)
774
            ->filter(
775
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
776
                    foreach ($devPackages as $devPackage) {
777
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
778
                            // File belongs to the dev package
779
                            return false;
780
                        }
781
                    }
782
783
                    return true;
784
                }
785
            )
786
            ->ignoreVCS(true)
787
        ;
788
789
        $normalizedConfig = (function (array $config, Finder $finder): array {
790
            $normalizedConfig = [];
791
792
            foreach ($config as $method => $arguments) {
793
                $method = trim($method);
794
                $arguments = (array) $arguments;
795
796
                Assertion::methodExists(
797
                    $method,
798
                    $finder,
799
                    'The method "Finder::%s" does not exist.'
800
                );
801
802
                $normalizedConfig[$method] = $arguments;
803
            }
804
805
            krsort($normalizedConfig);
806
807
            return $normalizedConfig;
808
        })((array) $config, $finder);
809
810
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
811
            $directory = self::normalizePath($directory, $basePath);
812
813
            if (is_link($directory)) {
814
                // TODO: add this to baberlei/assert
815
                throw new InvalidArgumentException(
816
                    sprintf(
817
                        'Cannot append the link "%s" to the Finder: links are not supported.',
818
                        $directory
819
                    )
820
                );
821
            }
822
823
            Assertion::directory($directory);
824
825
            return $directory;
826
        };
827
828
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
829
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
830
831
            if (is_link($fileOrDirectory)) {
832
                // TODO: add this to baberlei/assert
833
                throw new InvalidArgumentException(
834
                    sprintf(
835
                        'Cannot append the link "%s" to the Finder: links are not supported.',
836
                        $fileOrDirectory
837
                    )
838
                );
839
            }
840
841
            // TODO: add this to baberlei/assert
842
            if (false === file_exists($fileOrDirectory)) {
843
                throw new InvalidArgumentException(
844
                    sprintf(
845
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
846
                        $fileOrDirectory
847
                    )
848
                );
849
            }
850
851
            // TODO: add fileExists (as file or directory) to Assert
852
            if (false === is_file($fileOrDirectory)) {
853
                Assertion::directory($fileOrDirectory);
854
            } else {
855
                Assertion::file($fileOrDirectory);
856
            }
857
        };
858
859
        foreach ($normalizedConfig as $method => $arguments) {
860
            if ('in' === $method) {
861
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
862
            }
863
864
            if ('exclude' === $method) {
865
                $arguments = array_unique(array_map('trim', $arguments));
866
            }
867
868
            if ('append' === $method) {
869
                array_walk($arguments, $normalizeFileOrDirectory);
870
871
                $arguments = [$arguments];
872
            }
873
874
            foreach ($arguments as $argument) {
875
                $finder->$method($argument);
876
            }
877
        }
878
879
        return $finder;
880
    }
881
882
    /**
883
     * @param string[] $devPackages
884
     * @param string[] $filesToAppend
885
     *
886
     * @return string[][]
887
     */
888
    private static function retrieveAllDirectoriesToInclude(
889
        string $basePath,
890
        ?array $decodedJsonContents,
891
        array $devPackages,
892
        array $filesToAppend,
893
        array $excludedPaths
894
    ): array {
895
        $excludedPaths = array_flip($excludedPaths);
896
897
        $toString = function ($file): string {
898
            // @param string|SplFileInfo $file
899
            return (string) $file;
900
        };
901
902
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
903
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
904
        } else {
905
            $vendorDir = self::normalizePath('vendor', $basePath);
906
        }
907
908
        if (file_exists($vendorDir)) {
909
            $filesToAppend[] = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
910
911
            $vendorPackages = toArray(values(map(
912
                $toString,
913
                Finder::create()
914
                    ->in($vendorDir)
915
                    ->directories()
916
                    ->depth(1)
917
            )));
918
919
            $vendorPackages = array_diff($vendorPackages, $devPackages);
920
921
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
922
                $files = toArray(values(map(
923
                    $toString,
924
                    Finder::create()
925
                        ->in($basePath)
926
                        ->files()
927
                        ->depth(0)
928
                )));
929
930
                $directories = toArray(values(map(
931
                    $toString,
932
                    Finder::create()
933
                        ->in($basePath)
934
                        ->notPath('vendor')
935
                        ->directories()
936
                        ->depth(0)
937
                )));
938
939
                return [
940
                    array_merge($files, $filesToAppend),
941
                    array_merge($directories, $vendorPackages),
942
                ];
943
            }
944
945
            $paths = $vendorPackages;
946
        } else {
947
            $paths = [];
948
        }
949
950
        $autoload = $decodedJsonContents['autoload'] ?? [];
951
952
        if (array_key_exists('psr-4', $autoload)) {
953
            foreach ($autoload['psr-4'] as $path) {
954
                /** @var string|string[] $path */
955
                $composerPaths = (array) $path;
956
957
                foreach ($composerPaths as $composerPath) {
958
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
959
                }
960
            }
961
        }
962
963
        if (array_key_exists('psr-0', $autoload)) {
964
            foreach ($autoload['psr-0'] as $path) {
965
                /** @var string|string[] $path */
966
                $composerPaths = (array) $path;
967
968
                foreach ($composerPaths as $composerPath) {
969
                    if ('' !== trim($composerPath)) {
970
                        $paths[] = $composerPath;
971
                    }
972
                }
973
            }
974
        }
975
976
        if (array_key_exists('classmap', $autoload)) {
977
            foreach ($autoload['classmap'] as $path) {
978
                // @var string $path
979
                $paths[] = $path;
980
            }
981
        }
982
983
        $normalizePath = function (string $path) use ($basePath): string {
984
            return is_absolute_path($path)
985
                ? canonicalize($path)
986
                : self::normalizePath(trim($path, '/ '), $basePath)
987
            ;
988
        };
989
990
        if (array_key_exists('files', $autoload)) {
991
            foreach ($autoload['files'] as $path) {
992
                /** @var string $path */
993
                $path = $normalizePath($path);
994
995
                if (false === array_key_exists($path, $excludedPaths)) {
996
                    $filesToAppend[] = $path;
997
                }
998
            }
999
        }
1000
1001
        $files = $filesToAppend;
1002
        $directories = [];
1003
1004
        foreach ($paths as $path) {
1005
            $path = $normalizePath($path);
1006
1007
            if (is_file($path)) {
1008
                $files[] = $path;
1009
            } else {
1010
                $directories[] = $path;
1011
            }
1012
        }
1013
1014
        return [
1015
            array_unique($files),
1016
            array_unique($directories),
1017
        ];
1018
    }
1019
1020
    /**
1021
     * @param string   $basePath
1022
     * @param string[] $files
1023
     * @param string[] $directories
1024
     * @param string   $mainScriptPath
1025
     * @param Closure  $blacklistFilter
1026
     * @param string[] $excludedPaths
1027
     * @param string[] $devPackages
1028
     *
1029
     * @return SplFileInfo[]
1030
     */
1031
    private static function retrieveAllFiles(
1032
        string $basePath,
1033
        array $files,
1034
        array $directories,
1035
        string $mainScriptPath,
1036
        Closure $blacklistFilter,
1037
        array $excludedPaths,
1038
        array $devPackages
1039
    ): array {
1040
        $relativeDevPackages = array_map(
1041
            function (string $packagePath) use ($basePath): string {
1042
                return make_path_relative($packagePath, $basePath);
1043
            },
1044
            $devPackages
1045
        );
1046
1047
        $finder = Finder::create()
1048
            ->files()
1049
            ->notPath(make_path_relative($mainScriptPath, $basePath))
1050
            ->filter($blacklistFilter)
1051
            ->exclude($relativeDevPackages)
1052
            ->ignoreVCS(true)
1053
            ->ignoreDotFiles(true)
1054
            // Remove build files
1055
            ->notName('composer.json')
1056
            ->notName('Makefile')
1057
            ->notName('Vagrantfile')
1058
            ->notName('phpstan*.neon*')
1059
            ->notName('infection*.json*')
1060
            ->notName('humbug*.json*')
1061
            ->notName('easy-coding-standard.neon*')
1062
            ->notName('phpbench.json*')
1063
            ->notName('phpcs.xml*')
1064
            ->notName('scoper.inc*')
1065
            ->notName('box*.json*')
1066
            ->notName('codecov.yml*')
1067
            ->exclude('build')
1068
            ->exclude('dist')
1069
            ->exclude('example')
1070
            ->exclude('examples')
1071
            // Remove documentation
1072
            ->notName('*.md')
1073
            ->notName('README*')
1074
            ->notName('LICENSE*')
1075
            ->notName('UPGRADE*')
1076
            ->notName('CONTRIBUTING*')
1077
            ->notName('CHANGELOG*')
1078
            ->notName('AUTHOR*')
1079
            ->notName('CONDUCT*')
1080
            ->notName('TODO*')
1081
            ->exclude('doc')
1082
            ->exclude('docs')
1083
            ->exclude('documentation')
1084
            // Remove backup files
1085
            ->notName('*~')
1086
            ->notName('*.back')
1087
            ->notName('*.swp')
1088
            // Remove tests
1089
            ->notName('*Test.php')
1090
            ->exclude('test')
1091
            ->exclude('tests')
1092
            ->notName('/phpunit.*\.xml(.dist)?/')
1093
            ->notName('/behat.*\.yml(.dist)?/')
1094
            ->exclude('spec')
1095
            ->exclude('specs')
1096
            ->exclude('features')
1097
            // Remove CI config
1098
            ->exclude('travis')
1099
            ->notName('travis.yml')
1100
            ->notName('appveyor.yml')
1101
            ->notName('build.xml*')
1102
        ;
1103
1104
        $finder->append($files);
1105
        $finder->in($directories);
1106
1107
        $excludedPaths = array_unique(
1108
            array_filter(
1109
                array_map(
1110
                    function (string $path) use ($basePath): string {
1111
                        return make_path_relative($path, $basePath);
1112
                    },
1113
                    $excludedPaths
1114
                ),
1115
                function (string $path): bool {
1116
                    return '..' !== substr($path, 0, 2);
1117
                }
1118
            )
1119
        );
1120
1121
        foreach ($excludedPaths as $excludedPath) {
1122
            $finder->notPath($excludedPath);
1123
        }
1124
1125
        return array_unique(
1126
            toArray(
1127
                map(
1128
                    method('getRealPath'),
1129
                    $finder
1130
                )
1131
            )
1132
        );
1133
    }
1134
1135
    /**
1136
     * @param stdClass $raw
1137
     * @param string   $key      Config property name
1138
     * @param string   $basePath
1139
     *
1140
     * @return string[]
1141
     */
1142
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
1143
    {
1144
        if (false === isset($raw->{$key})) {
1145
            return [];
1146
        }
1147
1148
        $directories = $raw->{$key};
1149
1150
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1151
            $directory = self::normalizePath($directory, $basePath);
1152
1153
            if (is_link($directory)) {
1154
                // TODO: add this to baberlei/assert
1155
                throw new InvalidArgumentException(
1156
                    sprintf(
1157
                        'Cannot add the link "%s": links are not supported.',
1158
                        $directory
1159
                    )
1160
                );
1161
            }
1162
1163
            Assertion::directory(
1164
                $directory,
1165
                sprintf(
1166
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1167
                    $key
1168
                )
1169
            );
1170
1171
            return $directory;
1172
        };
1173
1174
        return array_map($normalizeDirectory, $directories);
1175
    }
1176
1177
    private static function normalizePath(string $file, string $basePath): string
1178
    {
1179
        return make_path_absolute(trim($file), $basePath);
1180
    }
1181
1182
    /**
1183
     * @return Compactor[]
1184
     */
1185
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
1186
    {
1187
        if (false === isset($raw->compactors)) {
1188
            return [];
1189
        }
1190
1191
        $compactorClasses = array_unique((array) $raw->compactors);
1192
1193
        return array_map(
1194
            function (string $class) use ($raw, $basePath): Compactor {
1195
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1196
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1197
1198
                if (Php::class === $class || LegacyPhp::class === $class) {
1199
                    return self::createPhpCompactor($raw);
1200
                }
1201
1202
                if (PhpScoperCompactor::class === $class) {
1203
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
1204
1205
                    return new PhpScoperCompactor(
1206
                        new SimpleScoper(
1207
                            create_scoper(),
1208
                            uniqid('_HumbugBox', false),
1209
                            $phpScoperConfig->getWhitelist(),
1210
                            $phpScoperConfig->getPatchers()
1211
                        )
1212
                    );
1213
                }
1214
1215
                return new $class();
1216
            },
1217
            $compactorClasses
1218
        );
1219
    }
1220
1221
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
1222
    {
1223
        if (false === isset($raw->compression)) {
1224
            return null;
1225
        }
1226
1227
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1228
1229
        Assertion::inArray(
1230
            $raw->compression,
1231
            $knownAlgorithmNames,
1232
            sprintf(
1233
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1234
                implode('", "', $knownAlgorithmNames)
1235
            )
1236
        );
1237
1238
        $value = get_phar_compression_algorithms()[$raw->compression];
1239
1240
        // Phar::NONE is not valid for compressFiles()
1241
        if (Phar::NONE === $value) {
1242
            return null;
1243
        }
1244
1245
        return $value;
1246
    }
1247
1248
    private static function retrieveFileMode(stdClass $raw): ?int
1249
    {
1250
        if (isset($raw->chmod)) {
1251
            return intval($raw->chmod, 8);
1252
        }
1253
1254
        return null;
1255
    }
1256
1257
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): string
1258
    {
1259
        if (isset($raw->main)) {
1260
            $main = $raw->main;
1261
        } else {
1262
            if (null === $decodedJsonContents
1263
                || false === array_key_exists('bin', $decodedJsonContents)
1264
                || false === $main = current($decodedJsonContents['bin'])
1265
            ) {
1266
                $main = self::DEFAULT_MAIN_SCRIPT;
1267
            }
1268
        }
1269
1270
        return self::normalizePath($main, $basePath);
1271
    }
1272
1273
    private static function retrieveMainScriptContents(string $mainScriptPath): string
1274
    {
1275
        $contents = file_contents($mainScriptPath);
1276
1277
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1278
        // PHAR entry point file.
1279
        return preg_replace('/^#!.*\s*/', '', $contents);
1280
    }
1281
1282
    private static function retrieveComposerFiles(string $basePath): array
1283
    {
1284
        $retrieveFileAndContents = function (string $file): array {
1285
            $json = new Json();
1286
1287
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1288
                return [null, null];
1289
            }
1290
1291
            try {
1292
                $contents = $json->decodeFile($file, true);
1293
            } catch (ParsingException $exception) {
1294
                throw new InvalidArgumentException(
1295
                    sprintf(
1296
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1297
                        $file,
1298
                        $exception->getMessage()
1299
                    ),
1300
                    0,
1301
                    $exception
1302
                );
1303
            }
1304
1305
            return [$file, $contents];
1306
        };
1307
1308
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1309
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1310
1311
        return [
1312
            [$composerJson, $composerJsonContents],
1313
            [$composerLock, $composerLockContents],
1314
        ];
1315
    }
1316
1317
    /**
1318
     * @return string[][]
1319
     */
1320
    private static function retrieveMap(stdClass $raw): array
1321
    {
1322
        if (false === isset($raw->map)) {
1323
            return [];
1324
        }
1325
1326
        $map = [];
1327
1328
        foreach ((array) $raw->map as $item) {
1329
            $processed = [];
1330
1331
            foreach ($item as $match => $replace) {
1332
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1333
            }
1334
1335
            if (isset($processed['_empty_'])) {
1336
                $processed[''] = $processed['_empty_'];
1337
1338
                unset($processed['_empty_']);
1339
            }
1340
1341
            $map[] = $processed;
1342
        }
1343
1344
        return $map;
1345
    }
1346
1347
    /**
1348
     * @return mixed
1349
     */
1350
    private static function retrieveMetadata(stdClass $raw)
1351
    {
1352
        if (isset($raw->metadata)) {
1353
            if (is_object($raw->metadata)) {
1354
                return (array) $raw->metadata;
1355
            }
1356
1357
            return $raw->metadata;
1358
        }
1359
1360
        return null;
1361
    }
1362
1363
    /**
1364
     * @return string[] The first element is the temporary output path and the second the real one
1365
     */
1366
    private static function retrieveOutputPath(stdClass $raw, string $basePath, string $mainScriptPath): array
1367
    {
1368
        if (isset($raw->output)) {
1369
            $path = $raw->output;
1370
        } else {
1371
            if (1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)) {
1372
                $path = $matches['main'].'.phar';
1373
            } else {
1374
                // Last resort, should not happen
1375
                $path = self::DEFAULT_ALIAS;
1376
            }
1377
        }
1378
1379
        $tmp = $real = self::normalizePath($path, $basePath);
1380
1381
        if ('.phar' !== substr($real, -5)) {
1382
            $tmp .= '.phar';
1383
        }
1384
1385
        return [$tmp, $real];
1386
    }
1387
1388
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1389
    {
1390
        // TODO: add check to not allow this setting without the private key path
1391
        if (isset($raw->{'key-pass'})
1392
            && is_string($raw->{'key-pass'})
1393
        ) {
1394
            return $raw->{'key-pass'};
1395
        }
1396
1397
        return null;
1398
    }
1399
1400
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1401
    {
1402
        // TODO: If passed need to check its existence
1403
        // Also need
1404
1405
        if (isset($raw->key)) {
1406
            return $raw->key;
1407
        }
1408
1409
        return null;
1410
    }
1411
1412
    private static function retrieveReplacements(stdClass $raw): array
1413
    {
1414
        // TODO: add exmample in the doc
1415
        // Add checks against the values
1416
        if (isset($raw->replacements)) {
1417
            return (array) $raw->replacements;
1418
        }
1419
1420
        return [];
1421
    }
1422
1423
    private static function retrieveProcessedReplacements(
1424
        array $replacements,
1425
        stdClass $raw,
1426
        ?string $file
1427
    ): array {
1428
        if (null === $file) {
1429
            return [];
1430
        }
1431
1432
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1433
            $replacements[$git] = self::retrieveGitHash($file);
1434
        }
1435
1436
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1437
            $replacements[$git] = self::retrieveGitHash($file, true);
1438
        }
1439
1440
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1441
            $replacements[$git] = self::retrieveGitTag($file);
1442
        }
1443
1444
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1445
            $replacements[$git] = self::retrieveGitVersion($file);
1446
        }
1447
1448
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1449
            $replacements[$date] = self::retrieveDatetimeNow(
1450
                self::retrieveDatetimeFormat($raw)
1451
            );
1452
        }
1453
1454
        $sigil = self::retrieveReplacementSigil($raw);
1455
1456
        foreach ($replacements as $key => $value) {
1457
            unset($replacements[$key]);
1458
            $replacements["$sigil$key$sigil"] = $value;
1459
        }
1460
1461
        return $replacements;
1462
    }
1463
1464
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1465
    {
1466
        if (isset($raw->{'git-commit'})) {
1467
            return $raw->{'git-commit'};
1468
        }
1469
1470
        return null;
1471
    }
1472
1473
    /**
1474
     * @param string $file
1475
     * @param bool   $short Use the short version
1476
     *
1477
     * @return string the commit hash
1478
     */
1479
    private static function retrieveGitHash(string $file, bool $short = false): string
1480
    {
1481
        return self::runGitCommand(
1482
            sprintf(
1483
                'git log --pretty="%s" -n1 HEAD',
1484
                $short ? '%h' : '%H'
1485
            ),
1486
            $file
1487
        );
1488
    }
1489
1490
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1491
    {
1492
        if (isset($raw->{'git-commit-short'})) {
1493
            return $raw->{'git-commit-short'};
1494
        }
1495
1496
        return null;
1497
    }
1498
1499
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1500
    {
1501
        if (isset($raw->{'git-tag'})) {
1502
            return $raw->{'git-tag'};
1503
        }
1504
1505
        return null;
1506
    }
1507
1508
    private static function retrieveGitTag(string $file): ?string
1509
    {
1510
        return self::runGitCommand('git describe --tags HEAD', $file);
1511
    }
1512
1513
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1514
    {
1515
        if (isset($raw->{'git-version'})) {
1516
            return $raw->{'git-version'};
1517
        }
1518
1519
        return null;
1520
    }
1521
1522
    private static function retrieveGitVersion(string $file): ?string
1523
    {
1524
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1525
        // to avoid messing around with that
1526
1527
        try {
1528
            return self::retrieveGitTag($file);
1529
        } catch (RuntimeException $exception) {
1530
            try {
1531
                return self::retrieveGitHash($file, true);
1532
            } catch (RuntimeException $exception) {
1533
                throw new RuntimeException(
1534
                    sprintf(
1535
                        'The tag or commit hash could not be retrieved from "%s": %s',
1536
                        dirname($file),
1537
                        $exception->getMessage()
1538
                    ),
1539
                    0,
1540
                    $exception
1541
                );
1542
            }
1543
        }
1544
    }
1545
1546
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1547
    {
1548
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1549
        // Also make sure the documentation is up to date after.
1550
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1551
        // be better to have only one element IMO like:
1552
        //
1553
        // "datetime": {
1554
        //   "value": "val",
1555
        //   "format": "Y-m-d"
1556
        // }
1557
        //
1558
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1559
        // if the datetime format is the default one it's ok; but in any case the format should not
1560
        // be added without the datetime value...
1561
1562
        if (isset($raw->{'datetime'})) {
1563
            return $raw->{'datetime'};
1564
        }
1565
1566
        return null;
1567
    }
1568
1569
    private static function retrieveDatetimeNow(string $format)
1570
    {
1571
        $now = new DateTimeImmutable('now');
1572
1573
        $datetime = $now->format($format);
1574
1575
        if (!$datetime) {
1576
            throw new InvalidArgumentException(
1577
                sprintf(
1578
                    '""%s" is not a valid PHP date format',
1579
                    $format
1580
                )
1581
            );
1582
        }
1583
1584
        return $datetime;
1585
    }
1586
1587
    private static function retrieveDatetimeFormat(stdClass $raw): string
1588
    {
1589
        if (isset($raw->{'datetime_format'})) {
1590
            return $raw->{'datetime_format'};
1591
        }
1592
1593
        return self::DEFAULT_DATETIME_FORMAT;
1594
    }
1595
1596
    private static function retrieveReplacementSigil(stdClass $raw)
1597
    {
1598
        if (isset($raw->{'replacement-sigil'})) {
1599
            return $raw->{'replacement-sigil'};
1600
        }
1601
1602
        return self::DEFAULT_REPLACEMENT_SIGIL;
1603
    }
1604
1605
    private static function retrieveShebang(stdClass $raw): ?string
1606
    {
1607
        if (false === array_key_exists('shebang', (array) $raw)) {
1608
            return self::DEFAULT_SHEBANG;
1609
        }
1610
1611
        if (null === $raw->shebang) {
1612
            return null;
1613
        }
1614
1615
        $shebang = trim($raw->shebang);
1616
1617
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1618
        Assertion::true(
1619
            '#!' === substr($shebang, 0, 2),
1620
            sprintf(
1621
                'The shebang line must start with "#!". Got "%s" instead',
1622
                $shebang
1623
            )
1624
        );
1625
1626
        return $shebang;
1627
    }
1628
1629
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1630
    {
1631
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1632
        // TODO: trigger a warning if the signing algorithm used is weak
1633
        // TODO: no longer accept strings & document BC break
1634
        if (false === isset($raw->algorithm)) {
1635
            return Phar::SHA1;
1636
        }
1637
1638
        if (false === defined('Phar::'.$raw->algorithm)) {
1639
            throw new InvalidArgumentException(
1640
                sprintf(
1641
                    'The signing algorithm "%s" is not supported.',
1642
                    $raw->algorithm
1643
                )
1644
            );
1645
        }
1646
1647
        return constant('Phar::'.$raw->algorithm);
1648
    }
1649
1650
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1651
    {
1652
        if (false === array_key_exists('banner', (array) $raw)) {
1653
            return self::DEFAULT_BANNER;
1654
        }
1655
1656
        if (null === $raw->banner) {
1657
            return null;
1658
        }
1659
1660
        $banner = $raw->banner;
1661
1662
        if (is_array($banner)) {
1663
            $banner = implode("\n", $banner);
1664
        }
1665
1666
        return $banner;
1667
    }
1668
1669
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1670
    {
1671
        if (false === isset($raw->{'banner-file'})) {
1672
            return null;
1673
        }
1674
1675
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1676
1677
        Assertion::file($bannerFile);
1678
1679
        return $bannerFile;
1680
    }
1681
1682
    private static function normalizeStubBannerContents(?string $contents): ?string
1683
    {
1684
        if (null === $contents) {
1685
            return null;
1686
        }
1687
1688
        $banner = explode("\n", $contents);
1689
        $banner = array_map('trim', $banner);
1690
1691
        return implode("\n", $banner);
1692
    }
1693
1694
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1695
    {
1696
        if (isset($raw->stub) && is_string($raw->stub)) {
1697
            $stubPath = make_path_absolute($raw->stub, $basePath);
1698
1699
            Assertion::file($stubPath);
1700
1701
            return $stubPath;
1702
        }
1703
1704
        return null;
1705
    }
1706
1707
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1708
    {
1709
        if (isset($raw->intercept)) {
1710
            return $raw->intercept;
1711
        }
1712
1713
        return false;
1714
    }
1715
1716
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1717
    {
1718
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1719
    }
1720
1721
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
1722
    {
1723
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
1724
    }
1725
1726
    private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerLock, bool $generateStub): bool
1727
    {
1728
        // TODO: emit warning when stub is not generated and check requirements is explicitly set to true
1729
        // TODO: emit warning when no composer lock is found but check requirements is explicitely set to true
1730
        if (false === $hasComposerLock) {
1731
            return false;
1732
        }
1733
1734
        return $raw->{'check-requirements'} ?? true;
1735
    }
1736
1737
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1738
    {
1739
        if (!isset($raw->{'php-scoper'})) {
1740
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1741
1742
            return file_exists($configFilePath)
1743
                ? PhpScoperConfiguration::load($configFilePath)
1744
                : PhpScoperConfiguration::load()
1745
             ;
1746
        }
1747
1748
        $configFile = $raw->phpScoper;
1749
1750
        Assertion::string($configFile);
1751
1752
        $configFilePath = make_path_absolute($configFile, $basePath);
1753
1754
        Assertion::file($configFilePath);
1755
        Assertion::readable($configFilePath);
1756
1757
        return PhpScoperConfiguration::load($configFilePath);
1758
    }
1759
1760
    /**
1761
     * Runs a Git command on the repository.
1762
     *
1763
     * @param string $command the command
1764
     *
1765
     * @return string the trimmed output from the command
1766
     */
1767
    private static function runGitCommand(string $command, string $file): string
1768
    {
1769
        $path = dirname($file);
1770
1771
        $process = new Process($command, $path);
1772
1773
        if (0 === $process->run()) {
1774
            return trim($process->getOutput());
1775
        }
1776
1777
        throw new RuntimeException(
1778
            sprintf(
1779
                'The tag or commit hash could not be retrieved from "%s": %s',
1780
                $path,
1781
                $process->getErrorOutput()
1782
            )
1783
        );
1784
    }
1785
1786
    private static function createPhpCompactor(stdClass $raw): Compactor
1787
    {
1788
        // TODO: false === not set; check & add test/doc
1789
        $tokenizer = new Tokenizer();
1790
1791
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1792
            $tokenizer->ignore(
1793
                (array) $raw->annotations->ignore
1794
            );
1795
        }
1796
1797
        return new Php($tokenizer);
1798
    }
1799
}
1800