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