Passed
Pull Request — master (#279)
by Théo
02:32
created

Configuration::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 79
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 41
dl 0
loc 79
rs 9.264
c 0
b 0
f 0
cc 2
nc 2
nop 31

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