Passed
Push — master ( f0b518...ee2495 )
by Théo
04:08 queued 02:07
created

Configuration::collectFiles()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 64
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 64
rs 9.0488
c 0
b 0
f 0
nc 4
nop 11
cc 5

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 Humbug\PhpScoper\Console\ApplicationFactory;
25
use Humbug\PhpScoper\Scoper;
26
use InvalidArgumentException;
27
use KevinGH\Box\Compactor\Php as PhpCompactor;
28
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
29
use KevinGH\Box\Composer\ComposerConfiguration;
30
use KevinGH\Box\Json\Json;
31
use KevinGH\Box\PhpScoper\SimpleScoper;
32
use Phar;
33
use RuntimeException;
34
use Seld\JsonLint\ParsingException;
35
use SplFileInfo;
36
use stdClass;
37
use Symfony\Component\Finder\Finder;
38
use Symfony\Component\Process\Process;
39
use const E_USER_DEPRECATED;
40
use function array_column;
41
use function array_diff;
42
use function array_filter;
43
use function array_key_exists;
44
use function array_keys;
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\map;
62
use function iter\toArray;
63
use function iter\values;
64
use function KevinGH\Box\FileSystem\canonicalize;
65
use function KevinGH\Box\FileSystem\file_contents;
66
use function KevinGH\Box\FileSystem\is_absolute_path;
67
use function KevinGH\Box\FileSystem\longest_common_base_path;
68
use function KevinGH\Box\FileSystem\make_path_absolute;
69
use function KevinGH\Box\FileSystem\make_path_relative;
70
use function preg_match;
71
use function property_exists;
72
use function realpath;
73
use function sprintf;
74
use function strtoupper;
75
use function substr;
76
use function trigger_error;
77
78
/**
79
 * @private
80
 */
81
final class Configuration
82
{
83
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
84
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
85
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
86
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
87
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
88
    private const DEFAULT_BANNER = <<<'BANNER'
89
Generated by Humbug Box.
90
91
@link https://github.com/humbug/box
92
BANNER;
93
    private const FILES_SETTINGS = [
94
        'directories',
95
        'finder',
96
    ];
97
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
98
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
99
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
100
101
    private const ALGORITHM_KEY = 'algorithm';
102
    private const ALIAS_KEY = 'alias';
103
    private const ANNOTATIONS_KEY = 'annotations';
104
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
105
    private const BANNER_KEY = 'banner';
106
    private const BANNER_FILE_KEY = 'banner-file';
107
    private const BASE_PATH_KEY = 'base-path';
108
    private const BLACKLIST_KEY = 'blacklist';
109
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
110
    private const CHMOD_KEY = 'chmod';
111
    private const COMPACTORS_KEY = 'compactors';
112
    private const COMPRESSION_KEY = 'compression';
113
    private const DATETIME_KEY = 'datetime';
114
    private const DATETIME_FORMAT_KEY = 'datetime-format';
115
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
116
    private const DIRECTORIES_KEY = 'directories';
117
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
118
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
119
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
120
    private const FILES_KEY = 'files';
121
    private const FILES_BIN_KEY = 'files-bin';
122
    private const FINDER_KEY = 'finder';
123
    private const FINDER_BIN_KEY = 'finder-bin';
124
    private const GIT_KEY = 'git';
125
    private const GIT_COMMIT_KEY = 'git-commit';
126
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
127
    private const GIT_TAG_KEY = 'git-tag';
128
    private const GIT_VERSION_KEY = 'git-version';
129
    private const INTERCEPT_KEY = 'intercept';
130
    private const KEY_KEY = 'key';
131
    private const KEY_PASS_KEY = 'key-pass';
132
    private const MAIN_KEY = 'main';
133
    private const MAP_KEY = 'map';
134
    private const METADATA_KEY = 'metadata';
135
    private const OUTPUT_KEY = 'output';
136
    private const PHP_SCOPER_KEY = 'php-scoper';
137
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
138
    private const REPLACEMENTS_KEY = 'replacements';
139
    private const SHEBANG_KEY = 'shebang';
140
    private const STUB_KEY = 'stub';
141
142
    private $file;
143
    private $fileMode;
144
    private $alias;
145
    private $basePath;
146
    private $composerJson;
147
    private $composerLock;
148
    private $files;
149
    private $binaryFiles;
150
    private $autodiscoveredFiles;
151
    private $dumpAutoload;
152
    private $excludeComposerFiles;
153
    private $compactors;
154
    private $compressionAlgorithm;
155
    private $mainScriptPath;
156
    private $mainScriptContents;
157
    private $fileMapper;
158
    private $metadata;
159
    private $tmpOutputPath;
160
    private $outputPath;
161
    private $privateKeyPassphrase;
162
    private $privateKeyPath;
163
    private $promptForPrivateKey;
164
    private $processedReplacements;
165
    private $shebang;
166
    private $signingAlgorithm;
167
    private $stubBannerContents;
168
    private $stubBannerPath;
169
    private $stubPath;
170
    private $isInterceptFileFuncs;
171
    private $isStubGenerated;
172
    private $checkRequirements;
173
    private $warnings;
174
    private $recommendations;
175
176
    public static function create(?string $file, stdClass $raw): self
177
    {
178
        $logger = new ConfigurationLogger();
179
180
        $basePath = self::retrieveBasePath($file, $raw, $logger);
181
182
        $composerFiles = self::retrieveComposerFiles($basePath);
183
184
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
185
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
186
187
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
188
189
        /** @var (string|null)[] $composerJson */
190
        $composerJson = $composerFiles[0];
191
        /** @var (string|null)[] $composerJson */
192
        $composerLock = $composerFiles[1];
193
194
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
195
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
196
197
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
198
199
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
200
201
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
202
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
203
204
        if (null !== $stubBannerPath) {
205
            $stubBannerContents = file_contents($stubBannerPath);
206
        }
207
208
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
209
210
        if (null !== $stubBannerPath && self::DEFAULT_BANNER === $stubBannerContents) {
211
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
212
        }
213
214
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
215
216
        $checkRequirements = self::retrieveCheckRequirements(
217
            $raw,
218
            null !== $composerJson[0],
219
            null !== $composerLock[0],
220
            false === $isStubGenerated && null === $stubPath,
221
            $logger
222
        );
223
224
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
0 ignored issues
show
Bug introduced by
It seems like $composerJson[1] can also be of type string; however, parameter $composerJsonDecodedContents of KevinGH\Box\Composer\Com...::retrieveDevPackages() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, /** @scrutinizer ignore-type */ $composerJson[1], $composerLock[1]);
Loading history...
225
226
        /**
227
         * @var string[]
228
         * @var Closure  $blacklistFilter
229
         */
230
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
231
            $raw,
232
            $basePath,
233
            $logger,
234
            $tmpOutputPath,
235
            $outputPath,
236
            $mainScriptPath
237
        );
238
239
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
240
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
241
242
        $filesAggregate = self::collectFiles(
243
            $raw,
244
            $basePath,
245
            $mainScriptPath,
246
            $blacklistFilter,
247
            $excludedPaths,
248
            $devPackages,
249
            $composerFiles,
250
            $composerJson,
251
            $autodiscoverFiles,
252
            $forceFilesAutodiscovery,
253
            $logger
254
        );
255
        $binaryFilesAggregate = self::collectBinaryFiles(
256
            $raw,
257
            $basePath,
258
            $mainScriptPath,
259
            $blacklistFilter,
260
            $excludedPaths,
261
            $devPackages,
262
            $logger
263
        );
264
265
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
266
267
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
268
269
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
270
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
271
272
        $fileMode = self::retrieveFileMode($raw, $logger);
273
274
        $map = self::retrieveMap($raw, $logger);
275
        $fileMapper = new MapFile($basePath, $map);
276
277
        $metadata = self::retrieveMetadata($raw, $logger);
278
279
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
280
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
281
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
282
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
283
284
        $replacements = self::retrieveReplacements($raw, $file, $logger);
285
286
        return new self(
287
            $file,
288
            $alias,
289
            $basePath,
290
            $composerJson,
291
            $composerLock,
292
            $filesAggregate,
293
            $binaryFilesAggregate,
294
            $autodiscoverFiles || $forceFilesAutodiscovery,
295
            $dumpAutoload,
296
            $excludeComposerFiles,
297
            $compactors,
298
            $compressionAlgorithm,
299
            $fileMode,
300
            $mainScriptPath,
301
            $mainScriptContents,
302
            $fileMapper,
303
            $metadata,
304
            $tmpOutputPath,
305
            $outputPath,
306
            $privateKeyPassphrase,
307
            $privateKeyPath,
308
            $promptForPrivateKey,
309
            $replacements,
310
            $shebang,
311
            $signingAlgorithm,
312
            $stubBannerContents,
313
            $stubBannerPath,
314
            $stubPath,
315
            $isInterceptsFileFuncs,
316
            $isStubGenerated,
317
            $checkRequirements,
318
            $logger->getWarnings(),
319
            $logger->getRecommendations()
320
        );
321
    }
322
323
    /**
324
     * @param null|string   $file
325
     * @param null|string   $alias
326
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
327
     *                                            path relative to it (the base path)
328
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
329
     *                                            string and the second element its decoded contents as an
330
     *                                            associative array.
331
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
332
     *                                            string and the second element its decoded contents as an
333
     *                                            associative array.
334
     * @param SplFileInfo[] $files                List of files
335
     * @param SplFileInfo[] $binaryFiles          List of binary files
336
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
337
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
338
     *                                            installed.json should be removed from the PHAR
339
     * @param Compactor[]   $compactors           List of file contents compactors
340
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
341
     * @param null|int      $fileMode             File mode in octal form
342
     * @param string        $mainScriptPath       The main script file path
343
     * @param string        $mainScriptContents   The processed content of the main script file
344
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
345
     * @param mixed         $metadata             The PHAR Metadata
346
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
347
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
348
     * @param null|string   $shebang              The shebang line
349
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
350
     * @param null|string   $stubBannerContents   The stub banner comment
351
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
352
     * @param null|string   $stubPath             The PHAR stub file path
353
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
354
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
355
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
356
     *                                            running
357
     * @param string[]      $warnings
358
     * @param string[]      $recommendations
359
     */
360
    private function __construct(
361
        ?string $file,
362
        string $alias,
363
        string $basePath,
364
        array $composerJson,
365
        array $composerLock,
366
        array $files,
367
        array $binaryFiles,
368
        bool $autodiscoveredFiles,
369
        bool $dumpAutoload,
370
        bool $excludeComposerFiles,
371
        array $compactors,
372
        ?int $compressionAlgorithm,
373
        ?int $fileMode,
374
        ?string $mainScriptPath,
375
        ?string $mainScriptContents,
376
        MapFile $fileMapper,
377
        $metadata,
378
        string $tmpOutputPath,
379
        string $outputPath,
380
        ?string $privateKeyPassphrase,
381
        ?string $privateKeyPath,
382
        bool $promptForPrivateKey,
383
        array $replacements,
384
        ?string $shebang,
385
        int $signingAlgorithm,
386
        ?string $stubBannerContents,
387
        ?string $stubBannerPath,
388
        ?string $stubPath,
389
        bool $isInterceptFileFuncs,
390
        bool $isStubGenerated,
391
        bool $checkRequirements,
392
        array $warnings,
393
        array $recommendations
394
    ) {
395
        Assertion::nullOrInArray(
396
            $compressionAlgorithm,
397
            get_phar_compression_algorithms(),
398
            sprintf(
399
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
400
                implode('", "', array_keys(get_phar_compression_algorithms()))
401
            )
402
        );
403
404
        if (null === $mainScriptPath) {
405
            Assertion::null($mainScriptContents);
406
        } else {
407
            Assertion::notNull($mainScriptContents);
408
        }
409
410
        $this->file = $file;
411
        $this->alias = $alias;
412
        $this->basePath = $basePath;
413
        $this->composerJson = $composerJson;
414
        $this->composerLock = $composerLock;
415
        $this->files = $files;
416
        $this->binaryFiles = $binaryFiles;
417
        $this->autodiscoveredFiles = $autodiscoveredFiles;
418
        $this->dumpAutoload = $dumpAutoload;
419
        $this->excludeComposerFiles = $excludeComposerFiles;
420
        $this->compactors = $compactors;
421
        $this->compressionAlgorithm = $compressionAlgorithm;
422
        $this->fileMode = $fileMode;
423
        $this->mainScriptPath = $mainScriptPath;
424
        $this->mainScriptContents = $mainScriptContents;
425
        $this->fileMapper = $fileMapper;
426
        $this->metadata = $metadata;
427
        $this->tmpOutputPath = $tmpOutputPath;
428
        $this->outputPath = $outputPath;
429
        $this->privateKeyPassphrase = $privateKeyPassphrase;
430
        $this->privateKeyPath = $privateKeyPath;
431
        $this->promptForPrivateKey = $promptForPrivateKey;
432
        $this->processedReplacements = $replacements;
433
        $this->shebang = $shebang;
434
        $this->signingAlgorithm = $signingAlgorithm;
435
        $this->stubBannerContents = $stubBannerContents;
436
        $this->stubBannerPath = $stubBannerPath;
437
        $this->stubPath = $stubPath;
438
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
439
        $this->isStubGenerated = $isStubGenerated;
440
        $this->checkRequirements = $checkRequirements;
441
        $this->warnings = $warnings;
442
        $this->recommendations = $recommendations;
443
    }
444
445
    public function getConfigurationFile(): ?string
446
    {
447
        return $this->file;
448
    }
449
450
    public function getAlias(): string
451
    {
452
        return $this->alias;
453
    }
454
455
    public function getBasePath(): string
456
    {
457
        return $this->basePath;
458
    }
459
460
    public function getComposerJson(): ?string
461
    {
462
        return $this->composerJson[0];
463
    }
464
465
    public function getDecodedComposerJsonContents(): ?array
466
    {
467
        return $this->composerJson[1];
468
    }
469
470
    public function getComposerLock(): ?string
471
    {
472
        return $this->composerLock[0];
473
    }
474
475
    public function getDecodedComposerLockContents(): ?array
476
    {
477
        return $this->composerLock[1];
478
    }
479
480
    /**
481
     * @return string[]
482
     */
483
    public function getFiles(): array
484
    {
485
        return $this->files;
486
    }
487
488
    /**
489
     * @return string[]
490
     */
491
    public function getBinaryFiles(): array
492
    {
493
        return $this->binaryFiles;
494
    }
495
496
    public function hasAutodiscoveredFiles(): bool
497
    {
498
        return $this->autodiscoveredFiles;
499
    }
500
501
    public function dumpAutoload(): bool
502
    {
503
        return $this->dumpAutoload;
504
    }
505
506
    public function excludeComposerFiles(): bool
507
    {
508
        return $this->excludeComposerFiles;
509
    }
510
511
    /**
512
     * @return Compactor[] the list of compactors
513
     */
514
    public function getCompactors(): array
515
    {
516
        return $this->compactors;
517
    }
518
519
    public function getCompressionAlgorithm(): ?int
520
    {
521
        return $this->compressionAlgorithm;
522
    }
523
524
    public function getFileMode(): ?int
525
    {
526
        return $this->fileMode;
527
    }
528
529
    public function hasMainScript(): bool
530
    {
531
        return null !== $this->mainScriptPath;
532
    }
533
534
    public function getMainScriptPath(): string
535
    {
536
        Assertion::notNull(
537
            $this->mainScriptPath,
538
            'Cannot retrieve the main script path: no main script configured.'
539
        );
540
541
        return $this->mainScriptPath;
542
    }
543
544
    public function getMainScriptContents(): string
545
    {
546
        Assertion::notNull(
547
            $this->mainScriptPath,
548
            'Cannot retrieve the main script contents: no main script configured.'
549
        );
550
551
        return $this->mainScriptContents;
552
    }
553
554
    public function checkRequirements(): bool
555
    {
556
        return $this->checkRequirements;
557
    }
558
559
    public function getTmpOutputPath(): string
560
    {
561
        return $this->tmpOutputPath;
562
    }
563
564
    public function getOutputPath(): string
565
    {
566
        return $this->outputPath;
567
    }
568
569
    public function getFileMapper(): MapFile
570
    {
571
        return $this->fileMapper;
572
    }
573
574
    /**
575
     * @return mixed
576
     */
577
    public function getMetadata()
578
    {
579
        return $this->metadata;
580
    }
581
582
    public function getPrivateKeyPassphrase(): ?string
583
    {
584
        return $this->privateKeyPassphrase;
585
    }
586
587
    public function getPrivateKeyPath(): ?string
588
    {
589
        return $this->privateKeyPath;
590
    }
591
592
    /**
593
     * @deprecated Use promptForPrivateKey() instead
594
     */
595
    public function isPrivateKeyPrompt(): bool
596
    {
597
        return $this->promptForPrivateKey;
598
    }
599
600
    public function promptForPrivateKey(): bool
601
    {
602
        return $this->promptForPrivateKey;
603
    }
604
605
    /**
606
     * @return scalar[]
607
     */
608
    public function getReplacements(): array
609
    {
610
        return $this->processedReplacements;
611
    }
612
613
    public function getShebang(): ?string
614
    {
615
        return $this->shebang;
616
    }
617
618
    public function getSigningAlgorithm(): int
619
    {
620
        return $this->signingAlgorithm;
621
    }
622
623
    public function getStubBannerContents(): ?string
624
    {
625
        return $this->stubBannerContents;
626
    }
627
628
    public function getStubBannerPath(): ?string
629
    {
630
        return $this->stubBannerPath;
631
    }
632
633
    public function getStubPath(): ?string
634
    {
635
        return $this->stubPath;
636
    }
637
638
    public function isInterceptFileFuncs(): bool
639
    {
640
        return $this->isInterceptFileFuncs;
641
    }
642
643
    public function isStubGenerated(): bool
644
    {
645
        return $this->isStubGenerated;
646
    }
647
648
    /**
649
     * @return string[]
650
     */
651
    public function getWarnings(): array
652
    {
653
        return $this->warnings;
654
    }
655
656
    /**
657
     * @return string[]
658
     */
659
    public function getRecommendations(): array
660
    {
661
        return $this->recommendations;
662
    }
663
664
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
665
    {
666
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
667
668
        if (false === isset($raw->{self::ALIAS_KEY})) {
669
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
670
        }
671
672
        $alias = trim($raw->{self::ALIAS_KEY});
673
674
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
675
676
        if ($userStubUsed) {
677
            $logger->addWarning(
678
                sprintf(
679
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
680
                    self::ALIAS_KEY
681
                )
682
            );
683
        }
684
685
        return $alias;
686
    }
687
688
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
689
    {
690
        if (null === $file) {
691
            return getcwd();
692
        }
693
694
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
695
            return realpath(dirname($file));
696
        }
697
698
        $basePath = trim($raw->{self::BASE_PATH_KEY});
699
700
        Assertion::directory(
701
            $basePath,
702
            'The base path "%s" is not a directory or does not exist.'
703
        );
704
705
        $basePath = realpath($basePath);
706
        $defaultPath = realpath(dirname($file));
707
708
        if ($basePath === $defaultPath) {
709
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
710
        }
711
712
        return $basePath;
713
    }
714
715
    /**
716
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
717
     */
718
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
719
    {
720
        if (null === $file) {
721
            return true;
722
        }
723
724
        $associativeRaw = (array) $raw;
725
726
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
727
    }
728
729
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
730
    {
731
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
732
733
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
734
    }
735
736
    private static function retrieveBlacklistFilter(
737
        stdClass $raw,
738
        string $basePath,
739
        ConfigurationLogger $logger,
740
        ?string ...$excludedPaths
741
    ): array {
742
        $blacklist = self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths);
743
744
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
745
            if ($file->isLink()) {
746
                return false;
747
            }
748
749
            if (false === $file->getRealPath()) {
750
                return false;
751
            }
752
753
            if (in_array($file->getRealPath(), $blacklist, true)) {
754
                return false;
755
            }
756
757
            return null;
758
        };
759
760
        return [$blacklist, $blacklistFilter];
761
    }
762
763
    /**
764
     * @param stdClass        $raw
765
     * @param string          $basePath
766
     * @param null[]|string[] $excludedPaths
767
     *
768
     * @return string[]
769
     */
770
    private static function retrieveBlacklist(
771
        stdClass $raw,
772
        string $basePath,
773
        ConfigurationLogger $logger,
774
        ?string ...$excludedPaths
775
    ): array {
776
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
777
778
        /** @var string[] $blacklist */
779
        $blacklist = array_merge(
780
            array_filter($excludedPaths),
781
            $raw->{self::BLACKLIST_KEY} ?? []
782
        );
783
784
        $normalizedBlacklist = [];
785
786
        foreach ($blacklist as $file) {
787
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
788
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
789
        }
790
791
        return array_unique($normalizedBlacklist);
792
    }
793
794
    /**
795
     * @param string[] $excludedPaths
796
     * @param string[] $devPackages
797
     *
798
     * @return SplFileInfo[]
799
     */
800
    private static function collectFiles(
801
        stdClass $raw,
802
        string $basePath,
803
        ?string $mainScriptPath,
804
        Closure $blacklistFilter,
805
        array $excludedPaths,
806
        array $devPackages,
807
        array $composerFiles,
808
        array $composerJson,
809
        bool $autodiscoverFiles,
810
        bool $forceFilesAutodiscovery,
811
        ConfigurationLogger $logger
812
    ): array {
813
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $mainScriptPath, $logger)];
814
815
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
816
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
817
                $basePath,
818
                $composerJson[1],
819
                $devPackages,
820
                array_filter(
821
                    array_column($composerFiles, 0)
822
                ),
823
                $excludedPaths
824
            );
825
826
            $files[] = $filesToAppend;
827
828
            $files[] = self::retrieveAllFiles(
829
                $basePath,
830
                $directories,
831
                $mainScriptPath,
832
                $blacklistFilter,
833
                $excludedPaths,
834
                $devPackages
835
            );
836
        }
837
838
        if (false === $autodiscoverFiles) {
839
            $files[] = self::retrieveDirectories(
840
                $raw,
841
                self::DIRECTORIES_KEY,
842
                $basePath,
843
                $blacklistFilter,
844
                $excludedPaths,
845
                $logger
846
            );
847
848
            $filesFromFinders = self::retrieveFilesFromFinders(
849
                $raw,
850
                self::FINDER_KEY,
851
                $basePath,
852
                $blacklistFilter,
853
                $devPackages,
854
                $logger
855
            );
856
857
            foreach ($filesFromFinders as $filesFromFinder) {
858
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
859
                $files[] = $filesFromFinder;
860
            }
861
        }
862
863
        return self::retrieveFilesAggregate(...$files);
864
    }
865
866
    /**
867
     * @param string[] $excludedPaths
868
     * @param string[] $devPackages
869
     *
870
     * @return SplFileInfo[]
871
     */
872
    private static function collectBinaryFiles(
873
        stdClass $raw,
874
        string $basePath,
875
        ?string $mainScriptPath,
876
        Closure $blacklistFilter,
877
        array $excludedPaths,
878
        array $devPackages,
879
        ConfigurationLogger $logger
880
    ): array {
881
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $mainScriptPath, $logger);
882
883
        $binaryDirectories = self::retrieveDirectories(
884
            $raw,
885
            self::DIRECTORIES_BIN_KEY,
886
            $basePath,
887
            $blacklistFilter,
888
            $excludedPaths,
889
            $logger
890
        );
891
892
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
893
            $raw,
894
            self::FINDER_BIN_KEY,
895
            $basePath,
896
            $blacklistFilter,
897
            $devPackages,
898
            $logger
899
        );
900
901
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
902
    }
903
904
    /**
905
     * @return SplFileInfo[]
906
     */
907
    private static function retrieveFiles(
908
        stdClass $raw,
909
        string $key,
910
        string $basePath,
911
        array $composerFiles,
912
        ?string $mainScriptPath,
913
        ConfigurationLogger $logger
914
    ): array {
915
        self::checkIfDefaultValue($logger, $raw, $key, []);
916
917
        $files = [];
918
919
        if (isset($composerFiles[0][0])) {
920
            $files[] = $composerFiles[0][0];
921
        }
922
923
        if (isset($composerFiles[1][1])) {
924
            $files[] = $composerFiles[1][0];
925
        }
926
927
        if (false === isset($raw->{$key})) {
928
            return $files;
929
        }
930
931
        if ([] === (array) $raw->{$key}) {
932
            return $files;
933
        }
934
935
        $files = array_merge((array) $raw->{$key}, $files);
936
937
        Assertion::allString($files);
938
939
        $normalizePath = function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
940
            $file = self::normalizePath($file, $basePath);
941
942
            Assertion::false(
943
                is_link($file),
944
                sprintf(
945
                    'Cannot add the link "%s": links are not supported.',
946
                    $file
947
                )
948
            );
949
950
            Assertion::file(
951
                $file,
952
                sprintf(
953
                    '"%s" must contain a list of existing files. Could not find "%%s".',
954
                    $key
955
                )
956
            );
957
958
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
959
        };
960
961
        return array_filter(array_map($normalizePath, $files));
962
    }
963
964
    /**
965
     * @param string   $key           Config property name
966
     * @param string[] $excludedPaths
967
     *
968
     * @return iterable|SplFileInfo[]
969
     */
970
    private static function retrieveDirectories(
971
        stdClass $raw,
972
        string $key,
973
        string $basePath,
974
        Closure $blacklistFilter,
975
        array $excludedPaths,
976
        ConfigurationLogger $logger
977
    ): iterable {
978
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
979
980
        if ([] !== $directories) {
981
            $finder = Finder::create()
982
                ->files()
983
                ->filter($blacklistFilter)
984
                ->ignoreVCS(true)
985
                ->in($directories)
986
            ;
987
988
            foreach ($excludedPaths as $excludedPath) {
989
                $finder->notPath($excludedPath);
990
            }
991
992
            return $finder;
993
        }
994
995
        return [];
996
    }
997
998
    /**
999
     * @param string[] $devPackages
1000
     *
1001
     * @return iterable[]|SplFileInfo[][]
1002
     */
1003
    private static function retrieveFilesFromFinders(
1004
        stdClass $raw,
1005
        string $key,
1006
        string $basePath,
1007
        Closure $blacklistFilter,
1008
        array $devPackages,
1009
        ConfigurationLogger $logger
1010
    ): array {
1011
        self::checkIfDefaultValue($logger, $raw, $key, []);
1012
1013
        if (false === isset($raw->{$key})) {
1014
            return [];
1015
        }
1016
1017
        $finder = $raw->{$key};
1018
1019
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1020
    }
1021
1022
    /**
1023
     * @param iterable[]|SplFileInfo[][] $fileIterators
1024
     *
1025
     * @return SplFileInfo[]
1026
     */
1027
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1028
    {
1029
        $files = [];
1030
1031
        foreach ($fileIterators as $fileIterator) {
1032
            foreach ($fileIterator as $file) {
1033
                $files[(string) $file] = $file;
1034
            }
1035
        }
1036
1037
        return array_values($files);
1038
    }
1039
1040
    /**
1041
     * @param string[] $devPackages
1042
     *
1043
     * @return Finder[]|SplFileInfo[][]
1044
     */
1045
    private static function processFinders(
1046
        array $findersConfig,
1047
        string $basePath,
1048
        Closure $blacklistFilter,
1049
        array $devPackages
1050
    ): array {
1051
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1052
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1053
        };
1054
1055
        return array_map($processFinderConfig, $findersConfig);
1056
    }
1057
1058
    /**
1059
     * @param string[] $devPackages
1060
     *
1061
     * @return Finder|SplFileInfo[]
1062
     */
1063
    private static function processFinder(
1064
        stdClass $config,
1065
        string $basePath,
1066
        Closure $blacklistFilter,
1067
        array $devPackages
1068
    ): Finder {
1069
        $finder = Finder::create()
1070
            ->files()
1071
            ->filter($blacklistFilter)
1072
            ->filter(
1073
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
1074
                    foreach ($devPackages as $devPackage) {
1075
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1076
                            // File belongs to the dev package
1077
                            return false;
1078
                        }
1079
                    }
1080
1081
                    return true;
1082
                }
1083
            )
1084
            ->ignoreVCS(true)
1085
        ;
1086
1087
        $normalizedConfig = (function (array $config, Finder $finder): array {
1088
            $normalizedConfig = [];
1089
1090
            foreach ($config as $method => $arguments) {
1091
                $method = trim($method);
1092
                $arguments = (array) $arguments;
1093
1094
                Assertion::methodExists(
1095
                    $method,
1096
                    $finder,
1097
                    'The method "Finder::%s" does not exist.'
1098
                );
1099
1100
                $normalizedConfig[$method] = $arguments;
1101
            }
1102
1103
            krsort($normalizedConfig);
1104
1105
            return $normalizedConfig;
1106
        })((array) $config, $finder);
1107
1108
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
1109
            $directory = self::normalizePath($directory, $basePath);
1110
1111
            Assertion::false(
1112
                is_link($directory),
1113
                sprintf(
1114
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1115
                    $directory
1116
                )
1117
            );
1118
1119
            Assertion::directory($directory);
1120
1121
            return $directory;
1122
        };
1123
1124
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1125
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
1126
1127
            Assertion::false(
1128
                is_link($fileOrDirectory),
1129
                sprintf(
1130
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1131
                    $fileOrDirectory
1132
                )
1133
            );
1134
1135
            Assertion::true(
1136
                file_exists($fileOrDirectory),
1137
                sprintf(
1138
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1139
                    $fileOrDirectory
1140
                )
1141
            );
1142
1143
            if (false === is_file($fileOrDirectory)) {
1144
                Assertion::directory($fileOrDirectory);
1145
            } else {
1146
                Assertion::file($fileOrDirectory);
1147
            }
1148
1149
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1150
                $fileOrDirectory = null;
1151
            }
1152
        };
1153
1154
        foreach ($normalizedConfig as $method => $arguments) {
1155
            if ('in' === $method) {
1156
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1157
            }
1158
1159
            if ('exclude' === $method) {
1160
                $arguments = array_unique(array_map('trim', $arguments));
1161
            }
1162
1163
            if ('append' === $method) {
1164
                array_walk($arguments, $normalizeFileOrDirectory);
1165
1166
                $arguments = [array_filter($arguments)];
1167
            }
1168
1169
            foreach ($arguments as $argument) {
1170
                $finder->$method($argument);
1171
            }
1172
        }
1173
1174
        return $finder;
1175
    }
1176
1177
    /**
1178
     * @param string[] $devPackages
1179
     * @param string[] $filesToAppend
1180
     *
1181
     * @return string[][]
1182
     */
1183
    private static function retrieveAllDirectoriesToInclude(
1184
        string $basePath,
1185
        ?array $decodedJsonContents,
1186
        array $devPackages,
1187
        array $filesToAppend,
1188
        array $excludedPaths
1189
    ): array {
1190
        $toString = function ($file): string {
1191
            // @param string|SplFileInfo $file
1192
            return (string) $file;
1193
        };
1194
1195
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1196
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1197
        } else {
1198
            $vendorDir = self::normalizePath('vendor', $basePath);
1199
        }
1200
1201
        if (file_exists($vendorDir)) {
1202
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1203
            // dependencies are included in the `composer.json`
1204
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1205
1206
            if (file_exists($installedJsonFiles)) {
1207
                $filesToAppend[] = $installedJsonFiles;
1208
            }
1209
1210
            $vendorPackages = toArray(values(map(
1211
                $toString,
1212
                Finder::create()
1213
                    ->in($vendorDir)
1214
                    ->directories()
1215
                    ->depth(1)
1216
                    ->ignoreUnreadableDirs()
1217
                    ->filter(
1218
                        function (SplFileInfo $fileInfo): ?bool {
1219
                            if ($fileInfo->isLink()) {
1220
                                return false;
1221
                            }
1222
1223
                            return null;
1224
                        }
1225
                    )
1226
            )));
1227
1228
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1229
1230
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1231
                $files = toArray(values(map(
1232
                    $toString,
1233
                    Finder::create()
1234
                        ->in($basePath)
1235
                        ->files()
1236
                        ->depth(0)
1237
                )));
1238
1239
                $directories = toArray(values(map(
1240
                    $toString,
1241
                    Finder::create()
1242
                        ->in($basePath)
1243
                        ->notPath('vendor')
1244
                        ->directories()
1245
                        ->depth(0)
1246
                )));
1247
1248
                return [
1249
                    array_merge($files, $filesToAppend),
1250
                    array_merge($directories, $vendorPackages),
1251
                ];
1252
            }
1253
1254
            $paths = $vendorPackages;
1255
        } else {
1256
            $paths = [];
1257
        }
1258
1259
        $autoload = $decodedJsonContents['autoload'] ?? [];
1260
1261
        if (array_key_exists('psr-4', $autoload)) {
1262
            foreach ($autoload['psr-4'] as $path) {
1263
                /** @var string|string[] $path */
1264
                $composerPaths = (array) $path;
1265
1266
                foreach ($composerPaths as $composerPath) {
1267
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1268
                }
1269
            }
1270
        }
1271
1272
        if (array_key_exists('psr-0', $autoload)) {
1273
            foreach ($autoload['psr-0'] as $path) {
1274
                /** @var string|string[] $path */
1275
                $composerPaths = (array) $path;
1276
1277
                foreach ($composerPaths as $composerPath) {
1278
                    if ('' !== trim($composerPath)) {
1279
                        $paths[] = $composerPath;
1280
                    }
1281
                }
1282
            }
1283
        }
1284
1285
        if (array_key_exists('classmap', $autoload)) {
1286
            foreach ($autoload['classmap'] as $path) {
1287
                // @var string $path
1288
                $paths[] = $path;
1289
            }
1290
        }
1291
1292
        $normalizePath = function (string $path) use ($basePath): string {
1293
            return is_absolute_path($path)
1294
                ? canonicalize($path)
1295
                : self::normalizePath(trim($path, '/ '), $basePath)
1296
            ;
1297
        };
1298
1299
        if (array_key_exists('files', $autoload)) {
1300
            foreach ($autoload['files'] as $path) {
1301
                // @var string $path
1302
                $path = $normalizePath($path);
1303
1304
                Assertion::file($path);
1305
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1306
1307
                $filesToAppend[] = $path;
1308
            }
1309
        }
1310
1311
        $files = $filesToAppend;
1312
        $directories = [];
1313
1314
        foreach ($paths as $path) {
1315
            $path = $normalizePath($path);
1316
1317
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1318
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1319
1320
            if (is_file($path)) {
1321
                $files[] = $path;
1322
            } else {
1323
                $directories[] = $path;
1324
            }
1325
        }
1326
1327
        [$files, $directories] = [
1328
            array_unique($files),
1329
            array_unique($directories),
1330
        ];
1331
1332
        return [
1333
            array_diff($files, $excludedPaths),
1334
            array_diff($directories, $excludedPaths),
1335
        ];
1336
    }
1337
1338
    /**
1339
     * @param string[] $files
1340
     * @param string[] $directories
1341
     * @param string[] $excludedPaths
1342
     * @param string[] $devPackages
1343
     *
1344
     * @return SplFileInfo[]
1345
     */
1346
    private static function retrieveAllFiles(
1347
        string $basePath,
1348
        array $directories,
1349
        ?string $mainScriptPath,
1350
        Closure $blacklistFilter,
1351
        array $excludedPaths,
1352
        array $devPackages
1353
    ): iterable {
1354
        if ([] === $directories) {
1355
            return [];
1356
        }
1357
1358
        $relativeDevPackages = array_map(
1359
            function (string $packagePath) use ($basePath): string {
1360
                return make_path_relative($packagePath, $basePath);
1361
            },
1362
            $devPackages
1363
        );
1364
1365
        $finder = Finder::create()
1366
            ->files()
1367
            ->filter($blacklistFilter)
1368
            ->exclude($relativeDevPackages)
1369
            ->ignoreVCS(true)
1370
            ->ignoreDotFiles(true)
1371
            // Remove build files
1372
            ->notName('composer.json')
1373
            ->notName('composer.lock')
1374
            ->notName('Makefile')
1375
            ->notName('Vagrantfile')
1376
            ->notName('phpstan*.neon*')
1377
            ->notName('infection*.json*')
1378
            ->notName('humbug*.json*')
1379
            ->notName('easy-coding-standard.neon*')
1380
            ->notName('phpbench.json*')
1381
            ->notName('phpcs.xml*')
1382
            ->notName('psalm.xml*')
1383
            ->notName('scoper.inc*')
1384
            ->notName('box*.json*')
1385
            ->notName('phpdoc*.xml*')
1386
            ->notName('codecov.yml*')
1387
            ->notName('Dockerfile')
1388
            ->exclude('build')
1389
            ->exclude('dist')
1390
            ->exclude('example')
1391
            ->exclude('examples')
1392
            // Remove documentation
1393
            ->notName('*.md')
1394
            ->notName('*.rst')
1395
            ->notName('/^readme(\..*+)?$/i')
1396
            ->notName('/^upgrade(\..*+)?$/i')
1397
            ->notName('/^contributing(\..*+)?$/i')
1398
            ->notName('/^changelog(\..*+)?$/i')
1399
            ->notName('/^authors?(\..*+)?$/i')
1400
            ->notName('/^conduct(\..*+)?$/i')
1401
            ->notName('/^todo(\..*+)?$/i')
1402
            ->exclude('doc')
1403
            ->exclude('docs')
1404
            ->exclude('documentation')
1405
            // Remove backup files
1406
            ->notName('*~')
1407
            ->notName('*.back')
1408
            ->notName('*.swp')
1409
            // Remove tests
1410
            ->notName('*Test.php')
1411
            ->exclude('test')
1412
            ->exclude('Test')
1413
            ->exclude('tests')
1414
            ->exclude('Tests')
1415
            ->notName('/phpunit.*\.xml(.dist)?/')
1416
            ->notName('/behat.*\.yml(.dist)?/')
1417
            ->exclude('spec')
1418
            ->exclude('specs')
1419
            ->exclude('features')
1420
            // Remove CI config
1421
            ->exclude('travis')
1422
            ->notName('travis.yml')
1423
            ->notName('appveyor.yml')
1424
            ->notName('build.xml*')
1425
        ;
1426
1427
        if (null !== $mainScriptPath) {
1428
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1429
        }
1430
1431
        $finder->in($directories);
1432
1433
        $excludedPaths = array_unique(
1434
            array_filter(
1435
                array_map(
1436
                    function (string $path) use ($basePath): string {
1437
                        return make_path_relative($path, $basePath);
1438
                    },
1439
                    $excludedPaths
1440
                ),
1441
                function (string $path): bool {
1442
                    return '..' !== substr($path, 0, 2);
1443
                }
1444
            )
1445
        );
1446
1447
        foreach ($excludedPaths as $excludedPath) {
1448
            $finder->notPath($excludedPath);
1449
        }
1450
1451
        return $finder;
0 ignored issues
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 SplFileInfo[].
Loading history...
1452
    }
1453
1454
    /**
1455
     * @param stdClass $raw
1456
     * @param string   $key      Config property name
1457
     * @param string   $basePath
1458
     *
1459
     * @return string[]
1460
     */
1461
    private static function retrieveDirectoryPaths(
1462
        stdClass $raw,
1463
        string $key,
1464
        string $basePath,
1465
        ConfigurationLogger $logger
1466
    ): array {
1467
        self::checkIfDefaultValue($logger, $raw, $key, []);
1468
1469
        if (false === isset($raw->{$key})) {
1470
            return [];
1471
        }
1472
1473
        $directories = $raw->{$key};
1474
1475
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1476
            $directory = self::normalizePath($directory, $basePath);
1477
1478
            Assertion::false(
1479
                is_link($directory),
1480
                sprintf(
1481
                    'Cannot add the link "%s": links are not supported.',
1482
                    $directory
1483
                )
1484
            );
1485
1486
            Assertion::directory(
1487
                $directory,
1488
                sprintf(
1489
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1490
                    $key
1491
                )
1492
            );
1493
1494
            return $directory;
1495
        };
1496
1497
        return array_map($normalizeDirectory, $directories);
1498
    }
1499
1500
    private static function normalizePath(string $file, string $basePath): string
1501
    {
1502
        return make_path_absolute(trim($file), $basePath);
1503
    }
1504
1505
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1506
    {
1507
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1508
1509
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1510
            return $composerJson;
1511
        }
1512
1513
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1514
1515
        if (false === $composerJson && $dumpAutoload) {
1516
            $logger->addWarning(
1517
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1518
                .' for it could not be found'
1519
            );
1520
1521
            return false;
1522
        }
1523
1524
        return $composerJson && false !== $dumpAutoload;
1525
    }
1526
1527
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1528
    {
1529
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1530
1531
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1532
    }
1533
1534
    /**
1535
     * @return Compactor[]
1536
     */
1537
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1538
    {
1539
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1540
1541
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1542
            return [];
1543
        }
1544
1545
        $compactorClasses = array_unique((array) $raw->{self::COMPACTORS_KEY});
1546
1547
        $compators = array_map(
1548
            function (string $class) use ($raw, $basePath, $logger): Compactor {
1549
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1550
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1551
1552
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1553
                    return self::createPhpCompactor($raw);
1554
                }
1555
1556
                if (PhpScoperCompactor::class === $class) {
1557
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
1558
1559
                    $prefix = null === $phpScoperConfig->getPrefix()
1560
                        ? unique_id('_HumbugBox')
1561
                        : $phpScoperConfig->getPrefix()
1562
                    ;
1563
1564
                    return new PhpScoperCompactor(
1565
                        new SimpleScoper(
1566
                            (new class() extends ApplicationFactory {
1567
                                public static function createScoper(): Scoper
1568
                                {
1569
                                    return parent::createScoper();
1570
                                }
1571
                            })::createScoper(),
1572
                            $prefix,
1573
                            $phpScoperConfig->getWhitelist(),
1574
                            $phpScoperConfig->getPatchers()
1575
                        )
1576
                    );
1577
                }
1578
1579
                return new $class();
1580
            },
1581
            $compactorClasses
1582
        );
1583
1584
        $scoperCompactor = false;
1585
1586
        foreach ($compators as $compactor) {
1587
            if ($compactor instanceof PhpScoperCompactor) {
1588
                $scoperCompactor = true;
1589
            }
1590
1591
            if ($compactor instanceof PhpCompactor) {
1592
                if (true === $scoperCompactor) {
1593
                    $logger->addRecommendation(
1594
                        sprintf(
1595
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1596
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1597
                        )
1598
                    );
1599
                }
1600
1601
                break;
1602
            }
1603
        }
1604
1605
        return $compators;
1606
    }
1607
1608
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1609
    {
1610
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY);
1611
1612
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1613
            return null;
1614
        }
1615
1616
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1617
1618
        Assertion::inArray(
1619
            $raw->{self::COMPRESSION_KEY},
1620
            $knownAlgorithmNames,
1621
            sprintf(
1622
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1623
                implode('", "', $knownAlgorithmNames)
1624
            )
1625
        );
1626
1627
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1628
1629
        // Phar::NONE is not valid for compressFiles()
1630
        if (Phar::NONE === $value) {
1631
            return null;
1632
        }
1633
1634
        return $value;
1635
    }
1636
1637
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1638
    {
1639
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1640
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1641
        }
1642
1643
        $defaultChmod = intval(0755, 8);
1644
1645
        if (isset($raw->{self::CHMOD_KEY})) {
1646
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1647
1648
            if ($defaultChmod === $chmod) {
1649
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1650
            }
1651
1652
            return $chmod;
1653
        }
1654
1655
        return $defaultChmod;
1656
    }
1657
1658
    private static function retrieveMainScriptPath(
1659
        stdClass $raw,
1660
        string $basePath,
1661
        ?array $decodedJsonContents,
1662
        ConfigurationLogger $logger
1663
    ): ?string {
1664
        $firstBin = false;
1665
1666
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1667
            /** @var false|string $firstBin */
1668
            $firstBin = current((array) $decodedJsonContents['bin']);
1669
1670
            if (false !== $firstBin) {
1671
                $firstBin = self::normalizePath($firstBin, $basePath);
1672
            }
1673
        }
1674
1675
        if (isset($raw->{self::MAIN_KEY})) {
1676
            $main = $raw->{self::MAIN_KEY};
1677
1678
            if (is_string($main)) {
1679
                $main = self::normalizePath($main, $basePath);
1680
1681
                if ($main === $firstBin) {
1682
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1683
                }
1684
            }
1685
        } else {
1686
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1687
        }
1688
1689
        if (is_bool($main)) {
1690
            Assertion::false(
1691
                $main,
1692
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1693
            );
1694
1695
            return null;
1696
        }
1697
1698
        Assertion::file($main);
1699
1700
        return $main;
1701
    }
1702
1703
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1704
    {
1705
        if (null === $mainScriptPath) {
1706
            return null;
1707
        }
1708
1709
        $contents = file_contents($mainScriptPath);
1710
1711
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1712
        // PHAR entry point file.
1713
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1714
        // case the main script can be ignored or disabled.
1715
        return preg_replace('/^#!.*\s*/', '', $contents);
1716
    }
1717
1718
    /**
1719
     * @return string|null[][]
1720
     */
1721
    private static function retrieveComposerFiles(string $basePath): array
1722
    {
1723
        $retrieveFileAndContents = function (string $file): array {
1724
            $json = new Json();
1725
1726
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1727
                return [null, null];
1728
            }
1729
1730
            try {
1731
                $contents = $json->decodeFile($file, true);
1732
            } catch (ParsingException $exception) {
1733
                throw new InvalidArgumentException(
1734
                    sprintf(
1735
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1736
                        $file,
1737
                        $exception->getMessage()
1738
                    ),
1739
                    0,
1740
                    $exception
1741
                );
1742
            }
1743
1744
            return [$file, $contents];
1745
        };
1746
1747
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1748
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1749
1750
        return [
1751
            [$composerJson, $composerJsonContents],
1752
            [$composerLock, $composerLockContents],
1753
        ];
1754
    }
1755
1756
    /**
1757
     * @return string[][]
1758
     */
1759
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1760
    {
1761
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1762
1763
        if (false === isset($raw->{self::MAP_KEY})) {
1764
            return [];
1765
        }
1766
1767
        $map = [];
1768
        $rawMap = (array) $raw->{self::MAP_KEY};
1769
1770
        foreach ($rawMap as $item) {
1771
            $processed = [];
1772
1773
            foreach ($item as $match => $replace) {
1774
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1775
            }
1776
1777
            if (isset($processed['_empty_'])) {
1778
                $processed[''] = $processed['_empty_'];
1779
1780
                unset($processed['_empty_']);
1781
            }
1782
1783
            $map[] = $processed;
1784
        }
1785
1786
        return $map;
1787
    }
1788
1789
    /**
1790
     * @return mixed
1791
     */
1792
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1793
    {
1794
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1795
1796
        if (false === isset($raw->{self::METADATA_KEY})) {
1797
            return null;
1798
        }
1799
1800
        $metadata = $raw->{self::METADATA_KEY};
1801
1802
        return is_object($metadata) ? (array) $metadata : $metadata;
1803
    }
1804
1805
    /**
1806
     * @return string[] The first element is the temporary output path and the second the final one
1807
     */
1808
    private static function retrieveOutputPath(
1809
        stdClass $raw,
1810
        string $basePath,
1811
        ?string $mainScriptPath,
1812
        ConfigurationLogger $logger
1813
    ): array {
1814
        $defaultPath = null;
1815
1816
        if (null !== $mainScriptPath
1817
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)
1818
        ) {
1819
            $defaultPath = $matches['main'].'.phar';
1820
        }
1821
1822
        if (isset($raw->{self::OUTPUT_KEY})) {
1823
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1824
1825
            if ($path === $defaultPath) {
1826
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1827
            }
1828
        } elseif (null !== $defaultPath) {
1829
            $path = $defaultPath;
1830
        } else {
1831
            // Last resort, should not happen
1832
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1833
        }
1834
1835
        $tmp = $real = $path;
1836
1837
        if ('.phar' !== substr($real, -5)) {
1838
            $tmp .= '.phar';
1839
        }
1840
1841
        return [$tmp, $real];
1842
    }
1843
1844
    private static function retrievePrivateKeyPath(
1845
        stdClass $raw,
1846
        string $basePath,
1847
        int $signingAlgorithm,
1848
        ConfigurationLogger $logger
1849
    ): ?string {
1850
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1851
            if (null === $raw->{self::KEY_KEY}) {
1852
                $logger->addRecommendation(
1853
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1854
                );
1855
            } else {
1856
                $logger->addWarning(
1857
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1858
                );
1859
            }
1860
1861
            return null;
1862
        }
1863
1864
        if (!isset($raw->{self::KEY_KEY})) {
1865
            Assertion::true(
1866
                Phar::OPENSSL !== $signingAlgorithm,
1867
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1868
            );
1869
1870
            return null;
1871
        }
1872
1873
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1874
1875
        Assertion::file($path);
1876
1877
        return $path;
1878
    }
1879
1880
    private static function retrievePrivateKeyPassphrase(
1881
        stdClass $raw,
1882
        int $algorithm,
1883
        ConfigurationLogger $logger
1884
    ): ?string {
1885
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1886
1887
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1888
            return null;
1889
        }
1890
1891
        /** @var null|false|string $keyPass */
1892
        $keyPass = $raw->{self::KEY_PASS_KEY};
1893
1894
        if (Phar::OPENSSL !== $algorithm) {
1895
            if (false === $keyPass || null === $keyPass) {
1896
                $logger->addRecommendation(
1897
                    sprintf(
1898
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1899
                        .'not "OPENSSL".',
1900
                        self::KEY_PASS_KEY
1901
                    )
1902
                );
1903
            } else {
1904
                $logger->addWarning(
1905
                    sprintf(
1906
                    'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1907
                        self::KEY_PASS_KEY
1908
                    )
1909
                );
1910
            }
1911
1912
            return null;
1913
        }
1914
1915
        return is_string($keyPass) ? $keyPass : null;
1916
    }
1917
1918
    /**
1919
     * @return scalar[]
1920
     */
1921
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1922
    {
1923
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1924
1925
        if (null === $file) {
1926
            return [];
1927
        }
1928
1929
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1930
1931
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1932
            $replacements[$git] = self::retrievePrettyGitTag($file);
1933
        }
1934
1935
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1936
            $replacements[$git] = self::retrieveGitHash($file);
1937
        }
1938
1939
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
1940
            $replacements[$git] = self::retrieveGitHash($file, true);
1941
        }
1942
1943
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
1944
            $replacements[$git] = self::retrieveGitTag($file);
1945
        }
1946
1947
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
1948
            $replacements[$git] = self::retrieveGitVersion($file);
1949
        }
1950
1951
        /**
1952
         * @var string
1953
         * @var bool   $valueSetByUser
1954
         */
1955
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
1956
1957
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
1958
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
1959
        } elseif ($valueSetByUser) {
1960
            $logger->addRecommendation(
1961
                sprintf(
1962
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
1963
                    self::DATETIME_FORMAT_KEY,
1964
                    self::DATETIME_KEY
1965
                )
1966
            );
1967
        }
1968
1969
        $sigil = self::retrieveReplacementSigil($raw, $logger);
1970
1971
        foreach ($replacements as $key => $value) {
1972
            unset($replacements[$key]);
1973
            $replacements[$sigil.$key.$sigil] = $value;
1974
        }
1975
1976
        return $replacements;
1977
    }
1978
1979
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1980
    {
1981
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
1982
    }
1983
1984
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1985
    {
1986
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
1987
    }
1988
1989
    /**
1990
     * @param string $file
1991
     * @param bool   $short Use the short version
1992
     *
1993
     * @return string the commit hash
1994
     */
1995
    private static function retrieveGitHash(string $file, bool $short = false): string
1996
    {
1997
        return self::runGitCommand(
1998
            sprintf(
1999
                'git log --pretty="%s" -n1 HEAD',
2000
                $short ? '%h' : '%H'
2001
            ),
2002
            $file
2003
        );
2004
    }
2005
2006
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2007
    {
2008
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2009
    }
2010
2011
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2012
    {
2013
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2014
    }
2015
2016
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2017
    {
2018
        self::checkIfDefaultValue($logger, $raw, $key);
2019
2020
        return $raw->{$key} ?? null;
2021
    }
2022
2023
    private static function retrieveGitTag(string $file): string
2024
    {
2025
        return self::runGitCommand('git describe --tags HEAD', $file);
2026
    }
2027
2028
    private static function retrievePrettyGitTag(string $file): string
2029
    {
2030
        $version = self::retrieveGitTag($file);
2031
2032
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2033
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2034
        }
2035
2036
        return $version;
2037
    }
2038
2039
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2040
    {
2041
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2042
    }
2043
2044
    private static function retrieveGitVersion(string $file): ?string
2045
    {
2046
        try {
2047
            return self::retrieveGitTag($file);
2048
        } catch (RuntimeException $exception) {
2049
            try {
2050
                return self::retrieveGitHash($file, true);
2051
            } catch (RuntimeException $exception) {
2052
                throw new RuntimeException(
2053
                    sprintf(
2054
                        'The tag or commit hash could not be retrieved from "%s": %s',
2055
                        dirname($file),
2056
                        $exception->getMessage()
2057
                    ),
2058
                    0,
2059
                    $exception
2060
                );
2061
            }
2062
        }
2063
    }
2064
2065
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2066
    {
2067
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2068
    }
2069
2070
    private static function retrieveDatetimeNow(string $format): string
2071
    {
2072
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2073
2074
        return $now->format($format);
2075
    }
2076
2077
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2078
    {
2079
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2080
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2081
2082
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2083
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2084
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2085
            @trigger_error(
2086
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2087
                E_USER_DEPRECATED
2088
            );
2089
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2090
2091
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2092
        } else {
2093
            $format = null;
2094
        }
2095
2096
        if (null !== $format) {
2097
            $formattedDate = (new DateTimeImmutable())->format($format);
2098
2099
            Assertion::false(
2100
                false === $formattedDate || $formattedDate === $format,
2101
                sprintf(
2102
                    'Expected the datetime format to be a valid format: "%s" is not',
2103
                    $format
2104
                )
2105
            );
2106
2107
            return [$format, true];
2108
        }
2109
2110
        return [self::DEFAULT_DATETIME_FORMAT, false];
2111
    }
2112
2113
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2114
    {
2115
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2116
    }
2117
2118
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2119
    {
2120
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2121
2122
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2123
            return self::DEFAULT_SHEBANG;
2124
        }
2125
2126
        $shebang = $raw->{self::SHEBANG_KEY};
2127
2128
        if (false === $shebang) {
2129
            if (false === $stubIsGenerated) {
2130
                $logger->addRecommendation(
2131
                    sprintf(
2132
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2133
                        .' being used',
2134
                        self::SHEBANG_KEY
2135
                    )
2136
                );
2137
            }
2138
2139
            return null;
2140
        }
2141
2142
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2143
2144
        $shebang = trim($shebang);
2145
2146
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2147
        Assertion::true(
2148
            '#!' === substr($shebang, 0, 2),
2149
            sprintf(
2150
                'The shebang line must start with "#!". Got "%s" instead',
2151
                $shebang
2152
            )
2153
        );
2154
2155
        if (false === $stubIsGenerated) {
2156
            $logger->addWarning(
2157
                sprintf(
2158
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2159
                    .' used',
2160
                    self::SHEBANG_KEY
2161
                )
2162
            );
2163
        }
2164
2165
        return $shebang;
2166
    }
2167
2168
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2169
    {
2170
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2171
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2172
        }
2173
2174
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2175
            return self::DEFAULT_SIGNING_ALGORITHM;
2176
        }
2177
2178
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2179
2180
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2181
2182
        Assertion::true(
2183
            defined('Phar::'.$algorithm),
2184
            sprintf(
2185
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2186
                $algorithm
2187
            )
2188
        );
2189
2190
        $algorithm = constant('Phar::'.$algorithm);
2191
2192
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2193
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2194
        }
2195
2196
        return $algorithm;
2197
    }
2198
2199
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2200
    {
2201
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::DEFAULT_BANNER);
2202
2203
        if (false === isset($raw->{self::BANNER_KEY})) {
2204
            return self::DEFAULT_BANNER;
2205
        }
2206
2207
        $banner = $raw->{self::BANNER_KEY};
2208
2209
        if (false === $banner) {
2210
            if (false === $stubIsGenerated) {
2211
                $logger->addRecommendation(
2212
                    sprintf(
2213
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2214
                        .'being used',
2215
                        self::BANNER_KEY
2216
                    )
2217
                );
2218
            }
2219
2220
            return null;
2221
        }
2222
2223
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2224
2225
        if (is_array($banner)) {
2226
            $banner = implode("\n", $banner);
2227
        }
2228
2229
        if (false === $stubIsGenerated) {
2230
            $logger->addWarning(
2231
                sprintf(
2232
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2233
                    self::BANNER_KEY
2234
                )
2235
            );
2236
        }
2237
2238
        return $banner;
2239
    }
2240
2241
    private static function retrieveStubBannerPath(
2242
        stdClass $raw,
2243
        string $basePath,
2244
        bool $stubIsGenerated,
2245
        ConfigurationLogger $logger
2246
    ): ?string {
2247
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2248
2249
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2250
            return null;
2251
        }
2252
2253
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2254
2255
        Assertion::file($bannerFile);
2256
2257
        if (false === $stubIsGenerated) {
2258
            $logger->addWarning(
2259
                sprintf(
2260
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2261
                    self::BANNER_FILE_KEY
2262
                )
2263
            );
2264
        }
2265
2266
        return $bannerFile;
2267
    }
2268
2269
    private static function normalizeStubBannerContents(?string $contents): ?string
2270
    {
2271
        if (null === $contents) {
2272
            return null;
2273
        }
2274
2275
        $banner = explode("\n", $contents);
2276
        $banner = array_map('trim', $banner);
2277
2278
        return implode("\n", $banner);
2279
    }
2280
2281
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2282
    {
2283
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2284
2285
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2286
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2287
2288
            Assertion::file($stubPath);
2289
2290
            return $stubPath;
2291
        }
2292
2293
        return null;
2294
    }
2295
2296
    private static function retrieveInterceptsFileFuncs(
2297
        stdClass $raw,
2298
        bool $stubIsGenerated,
2299
        ConfigurationLogger $logger
2300
    ): bool {
2301
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2302
2303
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2304
            return false;
2305
        }
2306
2307
        $intercept = $raw->{self::INTERCEPT_KEY};
2308
2309
        if ($intercept && false === $stubIsGenerated) {
2310
            $logger->addWarning(
2311
                sprintf(
2312
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2313
                    self::INTERCEPT_KEY
2314
                )
2315
            );
2316
        }
2317
2318
        return $intercept;
2319
    }
2320
2321
    private static function retrievePromptForPrivateKey(
2322
        stdClass $raw,
2323
        int $signingAlgorithm,
2324
        ConfigurationLogger $logger
2325
    ): bool {
2326
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2327
            if (Phar::OPENSSL !== $signingAlgorithm) {
2328
                $logger->addWarning(
2329
                    'A prompt for password for the private key has been requested but ignored since the signing '
2330
                    .'algorithm used is not "OPENSSL.'
2331
                );
2332
2333
                return false;
2334
            }
2335
2336
            return true;
2337
        }
2338
2339
        return false;
2340
    }
2341
2342
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2343
    {
2344
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2345
2346
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2347
    }
2348
2349
    private static function retrieveCheckRequirements(
2350
        stdClass $raw,
2351
        bool $hasComposerJson,
2352
        bool $hasComposerLock,
2353
        bool $pharStubUsed,
2354
        ConfigurationLogger $logger
2355
    ): bool {
2356
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2357
2358
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2359
            return $hasComposerJson || $hasComposerLock;
2360
        }
2361
2362
        /** @var bool $checkRequirements */
2363
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2364
2365
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2366
            $logger->addWarning(
2367
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2368
                .'be found.'
2369
            );
2370
2371
            return false;
2372
        }
2373
2374
        if ($checkRequirements && $pharStubUsed) {
2375
            $logger->addWarning(
2376
                sprintf(
2377
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2378
                    .'used.',
2379
                    self::CHECK_REQUIREMENTS_KEY
2380
                )
2381
            );
2382
        }
2383
2384
        return $checkRequirements;
2385
    }
2386
2387
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2388
    {
2389
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2390
2391
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2392
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2393
2394
            return file_exists($configFilePath)
2395
                ? PhpScoperConfiguration::load($configFilePath)
2396
                : PhpScoperConfiguration::load()
2397
             ;
2398
        }
2399
2400
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2401
2402
        Assertion::string($configFile);
2403
2404
        $configFilePath = make_path_absolute($configFile, $basePath);
2405
2406
        Assertion::file($configFilePath);
2407
        Assertion::readable($configFilePath);
2408
2409
        return PhpScoperConfiguration::load($configFilePath);
2410
    }
2411
2412
    /**
2413
     * Runs a Git command on the repository.
2414
     *
2415
     * @param string $command the command
2416
     *
2417
     * @return string the trimmed output from the command
2418
     */
2419
    private static function runGitCommand(string $command, string $file): string
2420
    {
2421
        $path = dirname($file);
2422
2423
        $process = new Process($command, $path);
2424
2425
        if (0 === $process->run()) {
2426
            return trim($process->getOutput());
2427
        }
2428
2429
        throw new RuntimeException(
2430
            sprintf(
2431
                'The tag or commit hash could not be retrieved from "%s": %s',
2432
                $path,
2433
                $process->getErrorOutput()
2434
            )
2435
        );
2436
    }
2437
2438
    private static function createPhpCompactor(stdClass $raw): Compactor
2439
    {
2440
        $tokenizer = new Tokenizer();
2441
2442
        if (false === empty($raw->{self::ANNOTATIONS_KEY}) && isset($raw->{self::ANNOTATIONS_KEY}->ignore)) {
2443
            $tokenizer->ignore(
2444
                (array) $raw->{self::ANNOTATIONS_KEY}->ignore
2445
            );
2446
        }
2447
2448
        return new PhpCompactor($tokenizer);
2449
    }
2450
2451
    private static function checkIfDefaultValue(
2452
        ConfigurationLogger $logger,
2453
        stdClass $raw,
2454
        string $key,
2455
        $defaultValue = null
2456
    ): void {
2457
        if (false === property_exists($raw, $key)) {
2458
            return;
2459
        }
2460
2461
        $value = $raw->{$key};
2462
2463
        if (null === $value
2464
            || (false === is_object($defaultValue) && $defaultValue === $value)
2465
            || (is_object($defaultValue) && $defaultValue == $value)
2466
        ) {
2467
            $logger->addRecommendation(
2468
                sprintf(
2469
                    'The "%s" setting can be omitted since is set to its default value',
2470
                    $key
2471
                )
2472
            );
2473
        }
2474
    }
2475
2476
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2477
    {
2478
        $logger->addRecommendation(
2479
            sprintf(
2480
                'The "%s" setting can be omitted since is set to its default value',
2481
                $key
2482
            )
2483
        );
2484
    }
2485
}
2486