Passed
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
            ->notPath('/doc.*/i')
1082
            // Remove backup files
1083
            ->notName('*~')
1084
            ->notName('*.back')
1085
            ->notName('*.swp')
1086
            // Remove tests
1087
            ->notName('*Test.php')
1088
            ->notPath('/test.*/i')
1089
            ->notName('/phpunit.*\.xml(.dist)?/')
1090
            ->notName('/behat.*\.yml(.dist)?/')
1091
            ->exclude('spec')
1092
            ->exclude('specs')
1093
            ->exclude('features')
1094
            // Remove CI config
1095
            ->exclude('travis')
1096
            ->notName('travis.yml')
1097
            ->notName('appveyor.yml')
1098
            ->notName('build.xml*')
1099
        ;
1100
1101
        $finder->append($files);
1102
        $finder->in($directories);
1103
1104
        $excludedPaths = array_unique(
1105
            array_filter(
1106
                array_map(
1107
                    function (string $path) use ($basePath): string {
1108
                        return make_path_relative($path, $basePath);
1109
                    },
1110
                    $excludedPaths
1111
                ),
1112
                function (string $path): bool {
1113
                    return '..' !== substr($path, 0, 2);
1114
                }
1115
            )
1116
        );
1117
1118
        foreach ($excludedPaths as $excludedPath) {
1119
            $finder->notPath($excludedPath);
1120
        }
1121
1122
        return array_unique(
1123
            toArray(
1124
                map(
1125
                    method('getRealPath'),
1126
                    $finder
1127
                )
1128
            )
1129
        );
1130
    }
1131
1132
    /**
1133
     * @param stdClass $raw
1134
     * @param string   $key      Config property name
1135
     * @param string   $basePath
1136
     *
1137
     * @return string[]
1138
     */
1139
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
1140
    {
1141
        if (false === isset($raw->{$key})) {
1142
            return [];
1143
        }
1144
1145
        $directories = $raw->{$key};
1146
1147
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1148
            $directory = self::normalizePath($directory, $basePath);
1149
1150
            if (is_link($directory)) {
1151
                // TODO: add this to baberlei/assert
1152
                throw new InvalidArgumentException(
1153
                    sprintf(
1154
                        'Cannot add the link "%s": links are not supported.',
1155
                        $directory
1156
                    )
1157
                );
1158
            }
1159
1160
            Assertion::directory(
1161
                $directory,
1162
                sprintf(
1163
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1164
                    $key
1165
                )
1166
            );
1167
1168
            return $directory;
1169
        };
1170
1171
        return array_map($normalizeDirectory, $directories);
1172
    }
1173
1174
    private static function normalizePath(string $file, string $basePath): string
1175
    {
1176
        return make_path_absolute(trim($file), $basePath);
1177
    }
1178
1179
    /**
1180
     * @return Compactor[]
1181
     */
1182
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
1183
    {
1184
        if (false === isset($raw->compactors)) {
1185
            return [];
1186
        }
1187
1188
        $compactorClasses = array_unique((array) $raw->compactors);
1189
1190
        return array_map(
1191
            function (string $class) use ($raw, $basePath): Compactor {
1192
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1193
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1194
1195
                if (Php::class === $class || LegacyPhp::class === $class) {
1196
                    return self::createPhpCompactor($raw);
1197
                }
1198
1199
                if (PhpScoperCompactor::class === $class) {
1200
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
1201
1202
                    return new PhpScoperCompactor(
1203
                        new SimpleScoper(
1204
                            create_scoper(),
1205
                            uniqid('_HumbugBox', false),
1206
                            $phpScoperConfig->getWhitelist(),
1207
                            $phpScoperConfig->getPatchers()
1208
                        )
1209
                    );
1210
                }
1211
1212
                return new $class();
1213
            },
1214
            $compactorClasses
1215
        );
1216
    }
1217
1218
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
1219
    {
1220
        if (false === isset($raw->compression)) {
1221
            return null;
1222
        }
1223
1224
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1225
1226
        Assertion::inArray(
1227
            $raw->compression,
1228
            $knownAlgorithmNames,
1229
            sprintf(
1230
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1231
                implode('", "', $knownAlgorithmNames)
1232
            )
1233
        );
1234
1235
        $value = get_phar_compression_algorithms()[$raw->compression];
1236
1237
        // Phar::NONE is not valid for compressFiles()
1238
        if (Phar::NONE === $value) {
1239
            return null;
1240
        }
1241
1242
        return $value;
1243
    }
1244
1245
    private static function retrieveFileMode(stdClass $raw): ?int
1246
    {
1247
        if (isset($raw->chmod)) {
1248
            return intval($raw->chmod, 8);
1249
        }
1250
1251
        return null;
1252
    }
1253
1254
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): string
1255
    {
1256
        if (isset($raw->main)) {
1257
            $main = $raw->main;
1258
        } else {
1259
            if (null === $decodedJsonContents
1260
                || false === array_key_exists('bin', $decodedJsonContents)
1261
                || false === $main = current($decodedJsonContents['bin'])
1262
            ) {
1263
                $main = self::DEFAULT_MAIN_SCRIPT;
1264
            }
1265
        }
1266
1267
        return self::normalizePath($main, $basePath);
1268
    }
1269
1270
    private static function retrieveMainScriptContents(string $mainScriptPath): string
1271
    {
1272
        $contents = file_contents($mainScriptPath);
1273
1274
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1275
        // PHAR entry point file.
1276
        return preg_replace('/^#!.*\s*/', '', $contents);
1277
    }
1278
1279
    private static function retrieveComposerFiles(string $basePath): array
1280
    {
1281
        $retrieveFileAndContents = function (string $file): array {
1282
            $json = new Json();
1283
1284
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1285
                return [null, null];
1286
            }
1287
1288
            try {
1289
                $contents = $json->decodeFile($file, true);
1290
            } catch (ParsingException $exception) {
1291
                throw new InvalidArgumentException(
1292
                    sprintf(
1293
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1294
                        $file,
1295
                        $exception->getMessage()
1296
                    ),
1297
                    0,
1298
                    $exception
1299
                );
1300
            }
1301
1302
            return [$file, $contents];
1303
        };
1304
1305
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1306
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1307
1308
        return [
1309
            [$composerJson, $composerJsonContents],
1310
            [$composerLock, $composerLockContents],
1311
        ];
1312
    }
1313
1314
    /**
1315
     * @return string[][]
1316
     */
1317
    private static function retrieveMap(stdClass $raw): array
1318
    {
1319
        if (false === isset($raw->map)) {
1320
            return [];
1321
        }
1322
1323
        $map = [];
1324
1325
        foreach ((array) $raw->map as $item) {
1326
            $processed = [];
1327
1328
            foreach ($item as $match => $replace) {
1329
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1330
            }
1331
1332
            if (isset($processed['_empty_'])) {
1333
                $processed[''] = $processed['_empty_'];
1334
1335
                unset($processed['_empty_']);
1336
            }
1337
1338
            $map[] = $processed;
1339
        }
1340
1341
        return $map;
1342
    }
1343
1344
    /**
1345
     * @return mixed
1346
     */
1347
    private static function retrieveMetadata(stdClass $raw)
1348
    {
1349
        if (isset($raw->metadata)) {
1350
            if (is_object($raw->metadata)) {
1351
                return (array) $raw->metadata;
1352
            }
1353
1354
            return $raw->metadata;
1355
        }
1356
1357
        return null;
1358
    }
1359
1360
    /**
1361
     * @return string[] The first element is the temporary output path and the second the real one
1362
     */
1363
    private static function retrieveOutputPath(stdClass $raw, string $basePath, string $mainScriptPath): array
1364
    {
1365
        if (isset($raw->output)) {
1366
            $path = $raw->output;
1367
        } else {
1368
            if (1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)) {
1369
                $path = $matches['main'].'.phar';
1370
            } else {
1371
                // Last resort, should not happen
1372
                $path = self::DEFAULT_ALIAS;
1373
            }
1374
        }
1375
1376
        $tmp = $real = self::normalizePath($path, $basePath);
1377
1378
        if ('.phar' !== substr($real, -5)) {
1379
            $tmp .= '.phar';
1380
        }
1381
1382
        return [$tmp, $real];
1383
    }
1384
1385
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1386
    {
1387
        // TODO: add check to not allow this setting without the private key path
1388
        if (isset($raw->{'key-pass'})
1389
            && is_string($raw->{'key-pass'})
1390
        ) {
1391
            return $raw->{'key-pass'};
1392
        }
1393
1394
        return null;
1395
    }
1396
1397
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1398
    {
1399
        // TODO: If passed need to check its existence
1400
        // Also need
1401
1402
        if (isset($raw->key)) {
1403
            return $raw->key;
1404
        }
1405
1406
        return null;
1407
    }
1408
1409
    private static function retrieveReplacements(stdClass $raw): array
1410
    {
1411
        // TODO: add exmample in the doc
1412
        // Add checks against the values
1413
        if (isset($raw->replacements)) {
1414
            return (array) $raw->replacements;
1415
        }
1416
1417
        return [];
1418
    }
1419
1420
    private static function retrieveProcessedReplacements(
1421
        array $replacements,
1422
        stdClass $raw,
1423
        ?string $file
1424
    ): array {
1425
        if (null === $file) {
1426
            return [];
1427
        }
1428
1429
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1430
            $replacements[$git] = self::retrieveGitHash($file);
1431
        }
1432
1433
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1434
            $replacements[$git] = self::retrieveGitHash($file, true);
1435
        }
1436
1437
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1438
            $replacements[$git] = self::retrieveGitTag($file);
1439
        }
1440
1441
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1442
            $replacements[$git] = self::retrieveGitVersion($file);
1443
        }
1444
1445
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1446
            $replacements[$date] = self::retrieveDatetimeNow(
1447
                self::retrieveDatetimeFormat($raw)
1448
            );
1449
        }
1450
1451
        $sigil = self::retrieveReplacementSigil($raw);
1452
1453
        foreach ($replacements as $key => $value) {
1454
            unset($replacements[$key]);
1455
            $replacements["$sigil$key$sigil"] = $value;
1456
        }
1457
1458
        return $replacements;
1459
    }
1460
1461
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1462
    {
1463
        if (isset($raw->{'git-commit'})) {
1464
            return $raw->{'git-commit'};
1465
        }
1466
1467
        return null;
1468
    }
1469
1470
    /**
1471
     * @param string $file
1472
     * @param bool   $short Use the short version
1473
     *
1474
     * @return string the commit hash
1475
     */
1476
    private static function retrieveGitHash(string $file, bool $short = false): string
1477
    {
1478
        return self::runGitCommand(
1479
            sprintf(
1480
                'git log --pretty="%s" -n1 HEAD',
1481
                $short ? '%h' : '%H'
1482
            ),
1483
            $file
1484
        );
1485
    }
1486
1487
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1488
    {
1489
        if (isset($raw->{'git-commit-short'})) {
1490
            return $raw->{'git-commit-short'};
1491
        }
1492
1493
        return null;
1494
    }
1495
1496
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1497
    {
1498
        if (isset($raw->{'git-tag'})) {
1499
            return $raw->{'git-tag'};
1500
        }
1501
1502
        return null;
1503
    }
1504
1505
    private static function retrieveGitTag(string $file): ?string
1506
    {
1507
        return self::runGitCommand('git describe --tags HEAD', $file);
1508
    }
1509
1510
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1511
    {
1512
        if (isset($raw->{'git-version'})) {
1513
            return $raw->{'git-version'};
1514
        }
1515
1516
        return null;
1517
    }
1518
1519
    private static function retrieveGitVersion(string $file): ?string
1520
    {
1521
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1522
        // to avoid messing around with that
1523
1524
        try {
1525
            return self::retrieveGitTag($file);
1526
        } catch (RuntimeException $exception) {
1527
            try {
1528
                return self::retrieveGitHash($file, true);
1529
            } catch (RuntimeException $exception) {
1530
                throw new RuntimeException(
1531
                    sprintf(
1532
                        'The tag or commit hash could not be retrieved from "%s": %s',
1533
                        dirname($file),
1534
                        $exception->getMessage()
1535
                    ),
1536
                    0,
1537
                    $exception
1538
                );
1539
            }
1540
        }
1541
    }
1542
1543
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1544
    {
1545
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1546
        // Also make sure the documentation is up to date after.
1547
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1548
        // be better to have only one element IMO like:
1549
        //
1550
        // "datetime": {
1551
        //   "value": "val",
1552
        //   "format": "Y-m-d"
1553
        // }
1554
        //
1555
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1556
        // if the datetime format is the default one it's ok; but in any case the format should not
1557
        // be added without the datetime value...
1558
1559
        if (isset($raw->{'datetime'})) {
1560
            return $raw->{'datetime'};
1561
        }
1562
1563
        return null;
1564
    }
1565
1566
    private static function retrieveDatetimeNow(string $format)
1567
    {
1568
        $now = new DateTimeImmutable('now');
1569
1570
        $datetime = $now->format($format);
1571
1572
        if (!$datetime) {
1573
            throw new InvalidArgumentException(
1574
                sprintf(
1575
                    '""%s" is not a valid PHP date format',
1576
                    $format
1577
                )
1578
            );
1579
        }
1580
1581
        return $datetime;
1582
    }
1583
1584
    private static function retrieveDatetimeFormat(stdClass $raw): string
1585
    {
1586
        if (isset($raw->{'datetime_format'})) {
1587
            return $raw->{'datetime_format'};
1588
        }
1589
1590
        return self::DEFAULT_DATETIME_FORMAT;
1591
    }
1592
1593
    private static function retrieveReplacementSigil(stdClass $raw)
1594
    {
1595
        if (isset($raw->{'replacement-sigil'})) {
1596
            return $raw->{'replacement-sigil'};
1597
        }
1598
1599
        return self::DEFAULT_REPLACEMENT_SIGIL;
1600
    }
1601
1602
    private static function retrieveShebang(stdClass $raw): ?string
1603
    {
1604
        if (false === array_key_exists('shebang', (array) $raw)) {
1605
            return self::DEFAULT_SHEBANG;
1606
        }
1607
1608
        if (null === $raw->shebang) {
1609
            return null;
1610
        }
1611
1612
        $shebang = trim($raw->shebang);
1613
1614
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1615
        Assertion::true(
1616
            '#!' === substr($shebang, 0, 2),
1617
            sprintf(
1618
                'The shebang line must start with "#!". Got "%s" instead',
1619
                $shebang
1620
            )
1621
        );
1622
1623
        return $shebang;
1624
    }
1625
1626
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1627
    {
1628
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1629
        // TODO: trigger a warning if the signing algorithm used is weak
1630
        // TODO: no longer accept strings & document BC break
1631
        if (false === isset($raw->algorithm)) {
1632
            return Phar::SHA1;
1633
        }
1634
1635
        if (false === defined('Phar::'.$raw->algorithm)) {
1636
            throw new InvalidArgumentException(
1637
                sprintf(
1638
                    'The signing algorithm "%s" is not supported.',
1639
                    $raw->algorithm
1640
                )
1641
            );
1642
        }
1643
1644
        return constant('Phar::'.$raw->algorithm);
1645
    }
1646
1647
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1648
    {
1649
        if (false === array_key_exists('banner', (array) $raw)) {
1650
            return self::DEFAULT_BANNER;
1651
        }
1652
1653
        if (null === $raw->banner) {
1654
            return null;
1655
        }
1656
1657
        $banner = $raw->banner;
1658
1659
        if (is_array($banner)) {
1660
            $banner = implode("\n", $banner);
1661
        }
1662
1663
        return $banner;
1664
    }
1665
1666
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1667
    {
1668
        if (false === isset($raw->{'banner-file'})) {
1669
            return null;
1670
        }
1671
1672
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1673
1674
        Assertion::file($bannerFile);
1675
1676
        return $bannerFile;
1677
    }
1678
1679
    private static function normalizeStubBannerContents(?string $contents): ?string
1680
    {
1681
        if (null === $contents) {
1682
            return null;
1683
        }
1684
1685
        $banner = explode("\n", $contents);
1686
        $banner = array_map('trim', $banner);
1687
1688
        return implode("\n", $banner);
1689
    }
1690
1691
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1692
    {
1693
        if (isset($raw->stub) && is_string($raw->stub)) {
1694
            $stubPath = make_path_absolute($raw->stub, $basePath);
1695
1696
            Assertion::file($stubPath);
1697
1698
            return $stubPath;
1699
        }
1700
1701
        return null;
1702
    }
1703
1704
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1705
    {
1706
        if (isset($raw->intercept)) {
1707
            return $raw->intercept;
1708
        }
1709
1710
        return false;
1711
    }
1712
1713
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1714
    {
1715
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1716
    }
1717
1718
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
1719
    {
1720
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
1721
    }
1722
1723
    private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerLock, bool $generateStub): bool
1724
    {
1725
        // TODO: emit warning when stub is not generated and check requirements is explicitly set to true
1726
        // TODO: emit warning when no composer lock is found but check requirements is explicitely set to true
1727
        if (false === $hasComposerLock) {
1728
            return false;
1729
        }
1730
1731
        return $raw->{'check-requirements'} ?? true;
1732
    }
1733
1734
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1735
    {
1736
        if (!isset($raw->{'php-scoper'})) {
1737
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1738
1739
            return file_exists($configFilePath)
1740
                ? PhpScoperConfiguration::load($configFilePath)
1741
                : PhpScoperConfiguration::load()
1742
             ;
1743
        }
1744
1745
        $configFile = $raw->phpScoper;
1746
1747
        Assertion::string($configFile);
1748
1749
        $configFilePath = make_path_absolute($configFile, $basePath);
1750
1751
        Assertion::file($configFilePath);
1752
        Assertion::readable($configFilePath);
1753
1754
        return PhpScoperConfiguration::load($configFilePath);
1755
    }
1756
1757
    /**
1758
     * Runs a Git command on the repository.
1759
     *
1760
     * @param string $command the command
1761
     *
1762
     * @return string the trimmed output from the command
1763
     */
1764
    private static function runGitCommand(string $command, string $file): string
1765
    {
1766
        $path = dirname($file);
1767
1768
        $process = new Process($command, $path);
1769
1770
        if (0 === $process->run()) {
1771
            return trim($process->getOutput());
1772
        }
1773
1774
        throw new RuntimeException(
1775
            sprintf(
1776
                'The tag or commit hash could not be retrieved from "%s": %s',
1777
                $path,
1778
                $process->getErrorOutput()
1779
            )
1780
        );
1781
    }
1782
1783
    private static function createPhpCompactor(stdClass $raw): Compactor
1784
    {
1785
        // TODO: false === not set; check & add test/doc
1786
        $tokenizer = new Tokenizer();
1787
1788
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1789
            $tokenizer->ignore(
1790
                (array) $raw->annotations->ignore
1791
            );
1792
        }
1793
1794
        return new Php($tokenizer);
1795
    }
1796
}
1797