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

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 67
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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