Passed
Pull Request — master (#201)
by Théo
03:03
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 69
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 69
rs 9.2083
c 0
b 0
f 0
cc 1
eloc 35
nc 1
nop 29

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