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