Passed
Pull Request — master (#146)
by Théo
02:26
created

Configuration::retrieveAllFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 9.2815
c 0
b 0
f 0
cc 2
eloc 40
nc 2
nop 7

How to fix   Long Method   

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:

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