Completed
Pull Request — master (#237)
by Théo
04:25 queued 01:49
created

Configuration::__construct()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 77
rs 8.9342
c 0
b 0
f 0
cc 2
eloc 40
nc 2
nop 30

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