Passed
Pull Request — master (#245)
by Théo
02:36
created

Configuration::__construct()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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