Passed
Push — master ( 812a1d...639186 )
by Théo
02:22
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 65
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 65
rs 9.3571
c 0
b 0
f 0
cc 1
eloc 33
nc 1
nop 27

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