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

Configuration::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 75
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 75
rs 9
c 0
b 0
f 0
cc 2
eloc 39
nc 2
nop 29

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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