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