Passed
Pull Request — master (#235)
by Théo
02:49
created

Configuration::__construct()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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