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