Configuration::processFinder()   C
last analyzed

Complexity

Conditions 12
Paths 17

Size

Total Lines 111
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 60
dl 0
loc 111
rs 6.446
c 0
b 0
f 0
cc 12
nc 17
nop 4

How to fix   Long Method    Complexity   

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:

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\Configuration;
16
17
use Closure;
18
use DateTimeImmutable;
19
use DateTimeZone;
20
use Fidry\FileSystem\FS;
21
use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration;
22
use InvalidArgumentException;
23
use JsonException;
24
use KevinGH\Box\Compactor\Compactor;
25
use KevinGH\Box\Compactor\Compactors;
26
use KevinGH\Box\Compactor\Php as PhpCompactor;
27
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
28
use KevinGH\Box\Composer\Artifact\ComposerArtifact;
29
use KevinGH\Box\Composer\Artifact\ComposerArtifacts;
30
use KevinGH\Box\Composer\Artifact\ComposerJson;
31
use KevinGH\Box\Composer\Artifact\ComposerLock;
32
use KevinGH\Box\Composer\ComposerConfiguration;
33
use KevinGH\Box\Json\Json;
34
use KevinGH\Box\MapFile;
35
use KevinGH\Box\Phar\CompressionAlgorithm;
36
use KevinGH\Box\Phar\SigningAlgorithm;
37
use KevinGH\Box\PhpScoper\ConfigurationFactory as PhpScoperConfigurationFactory;
38
use KevinGH\Box\PhpScoper\SerializableScoper;
39
use Phar;
40
use RuntimeException;
41
use SplFileInfo;
42
use stdClass;
43
use Symfony\Component\Filesystem\Path;
44
use Symfony\Component\Finder\Finder;
45
use Symfony\Component\Process\Exception\ProcessFailedException;
46
use Symfony\Component\Process\Process;
47
use Webmozart\Assert\Assert;
48
use function array_diff;
49
use function array_filter;
50
use function array_flip;
51
use function array_key_exists;
52
use function array_keys;
53
use function array_map;
54
use function array_merge;
55
use function array_unique;
56
use function array_values;
57
use function array_walk;
58
use function dirname;
59
use function explode;
60
use function file_exists;
61
use function getcwd;
62
use function implode;
63
use function in_array;
64
use function intval;
65
use function is_array;
66
use function is_bool;
67
use function is_file;
68
use function is_link;
69
use function is_object;
70
use function is_readable;
71
use function is_string;
72
use function iter\map;
73
use function iter\toArray;
74
use function iter\values;
75
use function KevinGH\Box\get_box_version;
76
use function KevinGH\Box\unique_id;
77
use function krsort;
78
use function preg_match;
79
use function preg_replace;
80
use function property_exists;
81
use function realpath;
82
use function Safe\json_decode;
83
use function sprintf;
84
use function str_starts_with;
85
use function trigger_error;
86
use function trim;
87
use const E_USER_DEPRECATED;
88
89
/**
90
 * @private
91
 */
92
final class Configuration
93
{
94
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
95
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
96
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
97
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
98
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
99
    private const DEFAULT_BANNER = <<<'BANNER'
100
        Generated by Humbug Box %s.
101
102
        @link https://github.com/humbug/box
103
        BANNER;
104
    private const FILES_SETTINGS = [
105
        'directories',
106
        'finder',
107
    ];
108
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
109
    private const DEFAULT_SIGNING_ALGORITHM = SigningAlgorithm::SHA512;
110
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
111
112
    private const DEFAULT_IGNORED_ANNOTATIONS = [
113
        'abstract',
114
        'access',
115
        'annotation',
116
        'api',
117
        'attribute',
118
        'attributes',
119
        'author',
120
        'category',
121
        'code',
122
        'codecoverageignore',
123
        'codecoverageignoreend',
124
        'codecoverageignorestart',
125
        'copyright',
126
        'deprec',
127
        'deprecated',
128
        'endcode',
129
        'example',
130
        'exception',
131
        'filesource',
132
        'final',
133
        'fixme',
134
        'global',
135
        'ignore',
136
        'ingroup',
137
        'inheritdoc',
138
        'internal',
139
        'license',
140
        'link',
141
        'magic',
142
        'method',
143
        'name',
144
        'override',
145
        'package',
146
        'package_version',
147
        'param',
148
        'private',
149
        'property',
150
        'required',
151
        'return',
152
        'see',
153
        'since',
154
        'static',
155
        'staticvar',
156
        'subpackage',
157
        'suppresswarnings',
158
        'target',
159
        'throw',
160
        'throws',
161
        'todo',
162
        'tutorial',
163
        'usedby',
164
        'uses',
165
        'var',
166
        'version',
167
    ];
168
169
    private const ALGORITHM_KEY = 'algorithm';
170
    private const ALIAS_KEY = 'alias';
171
    private const ANNOTATIONS_KEY = 'annotations';
172
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
173
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
174
    private const BANNER_KEY = 'banner';
175
    private const BANNER_FILE_KEY = 'banner-file';
176
    private const BASE_PATH_KEY = 'base-path';
177
    private const BLACKLIST_KEY = 'blacklist';
178
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
179
    private const CHMOD_KEY = 'chmod';
180
    private const COMPACTORS_KEY = 'compactors';
181
    private const COMPRESSION_KEY = 'compression';
182
    private const DATETIME_KEY = 'datetime';
183
    private const DATETIME_FORMAT_KEY = 'datetime-format';
184
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
185
    private const DIRECTORIES_KEY = 'directories';
186
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
187
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
188
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
189
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
190
    private const FILES_KEY = 'files';
191
    private const FILES_BIN_KEY = 'files-bin';
192
    private const FINDER_KEY = 'finder';
193
    private const FINDER_BIN_KEY = 'finder-bin';
194
    private const GIT_KEY = 'git';
195
    private const GIT_COMMIT_KEY = 'git-commit';
196
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
197
    private const GIT_TAG_KEY = 'git-tag';
198
    private const GIT_VERSION_KEY = 'git-version';
199
    private const INTERCEPT_KEY = 'intercept';
200
    private const KEY_KEY = 'key';
201
    private const KEY_PASS_KEY = 'key-pass';
202
    private const MAIN_KEY = 'main';
203
    private const MAP_KEY = 'map';
204
    private const METADATA_KEY = 'metadata';
205
    private const OUTPUT_KEY = 'output';
206
    private const PHP_SCOPER_KEY = 'php-scoper';
207
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
208
    private const REPLACEMENTS_KEY = 'replacements';
209
    private const SHEBANG_KEY = 'shebang';
210
    private const STUB_KEY = 'stub';
211
    private const TIMESTAMP = 'timestamp';
212
213
    private readonly ?string $mainScriptPath;
214
    private readonly ?string $mainScriptContents;
215
    private ?string $composerBin = null;
216
217
    public static function create(?string $file, stdClass $raw): self
218
    {
219
        $logger = new ConfigurationLogger();
220
221
        $basePath = self::retrieveBasePath($file, $raw, $logger);
222
223
        $composerArtifacts = self::retrieveComposerArtifacts($basePath);
224
225
        $dumpAutoload = self::retrieveDumpAutoload($raw, $composerArtifacts, $logger);
226
227
        $excludeComposerArtifacts = self::retrieveExcludeComposerArtifacts($raw, $logger);
228
229
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerArtifacts->composerJson, $logger);
230
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
231
232
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
233
234
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
235
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
236
237
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
238
239
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
240
241
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
242
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
243
244
        if (null !== $stubBannerPath) {
245
            $stubBannerContents = FS::getFileContents($stubBannerPath);
246
        }
247
248
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
249
250
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
251
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
252
        }
253
254
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
255
256
        $checkRequirements = self::retrieveCheckRequirements(
257
            $raw,
258
            null !== $composerArtifacts->composerJson?->path,
259
            null !== $composerArtifacts->composerLock?->path,
260
            false === $isStubGenerated && null === $stubPath,
261
            $logger,
262
        );
263
264
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
265
266
        $devPackages = ComposerConfiguration::retrieveDevPackages(
267
            $basePath,
268
            $composerArtifacts->composerJson,
269
            $composerArtifacts->composerLock,
270
            $excludeDevPackages,
271
        );
272
273
        /**
274
         * @var string[] $excludedPaths
275
         * @var Closure  $blacklistFilter
276
         */
277
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
278
            $raw,
279
            $basePath,
280
            $logger,
281
            $tmpOutputPath,
282
            $outputPath,
283
            $mainScriptPath,
284
        );
285
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
286
        // If you consider the following:
287
        //
288
        // {
289
        //   "files": ["file1"],
290
        //   "blacklist": ["file1"],
291
        // }
292
        //
293
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
294
        // and directories but the user should always have the possibility to force his way to include a file.
295
        //
296
        // The exception however, is for the following which is essential for the good functioning of Box
297
        $alwaysExcludedPaths = array_map(
298
            static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath),
299
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath]),
300
        );
301
302
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
303
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
304
305
        $filesAggregate = self::collectFiles(
306
            $raw,
307
            $basePath,
308
            $mainScriptPath,
309
            $blacklistFilter,
310
            $excludedPaths,
311
            $alwaysExcludedPaths,
312
            $devPackages,
313
            $composerArtifacts,
314
            $autodiscoverFiles,
315
            $forceFilesAutodiscovery,
316
            $logger,
317
        );
318
        $binaryFilesAggregate = self::collectBinaryFiles(
319
            $raw,
320
            $basePath,
321
            $blacklistFilter,
322
            $excludedPaths,
323
            $alwaysExcludedPaths,
324
            $devPackages,
325
            $logger,
326
        );
327
328
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
329
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
330
331
        $fileMode = self::retrieveFileMode($raw, $logger);
332
333
        $map = self::retrieveMap($raw, $logger);
334
        $fileMapper = new MapFile($basePath, $map);
335
336
        $metadata = self::retrieveMetadata($raw, $logger);
337
338
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
339
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
340
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
341
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
342
343
        $replacements = self::retrieveReplacements($raw, $file, $basePath, $logger);
344
345
        $timestamp = self::retrieveTimestamp($raw, $signingAlgorithm, $logger);
346
347
        return new self(
348
            $file,
349
            $alias,
350
            $basePath,
351
            $composerArtifacts->composerJson,
352
            $composerArtifacts->composerLock,
353
            $filesAggregate,
354
            $binaryFilesAggregate,
355
            $autodiscoverFiles || $forceFilesAutodiscovery,
356
            $dumpAutoload,
357
            $excludeComposerArtifacts,
358
            $excludeDevPackages,
359
            $compactors,
360
            $compressionAlgorithm,
361
            $fileMode,
362
            $mainScriptPath,
363
            $mainScriptContents,
364
            $fileMapper,
365
            $metadata,
366
            $tmpOutputPath,
367
            $outputPath,
368
            $privateKeyPassphrase,
369
            $privateKeyPath,
370
            $promptForPrivateKey,
371
            $replacements,
372
            $shebang,
373
            $signingAlgorithm,
374
            $stubBannerContents,
375
            $stubBannerPath,
376
            $stubPath,
377
            $isInterceptsFileFunctions,
378
            $isStubGenerated,
379
            $timestamp,
380
            $checkRequirements,
381
            $logger->getWarnings(),
382
            $logger->getRecommendations(),
383
        );
384
    }
385
386
    /**
387
     * @param string                 $basePath                 Utility to private the base path used and be able to retrieve a
388
     *                                                         path relative to it (the base path)
389
     * @param array                  $composerJson             The first element is the path to the `composer.json` file as a
390
     *                                                         string and the second element its decoded contents as an
391
     *                                                         associative array.
392
     * @param array                  $composerLock             The first element is the path to the `composer.lock` file as a
393
     *                                                         string and the second element its decoded contents as an
394
     *                                                         associative array.
395
     * @param SplFileInfo[]          $files                    List of files
396
     * @param SplFileInfo[]          $binaryFiles              List of binary files
397
     * @param bool                   $dumpAutoload             Whether the Composer autoloader should be dumped
398
     * @param bool                   $excludeComposerArtifacts Whether the Composer files composer.json, composer.lock and
399
     *                                                         installed.json should be removed from the PHAR
400
     * @param CompressionAlgorithm   $compressionAlgorithm     Compression algorithm constant value. See the \Phar class constants
401
     * @param null|int               $fileMode                 File mode in octal form
402
     * @param string                 $mainScriptPath           The main script file path
403
     * @param string                 $mainScriptContents       The processed content of the main script file
404
     * @param MapFile                $fileMapper               Utility to map the files from outside and inside the PHAR
405
     * @param mixed                  $metadata                 The PHAR Metadata
406
     * @param bool                   $promptForPrivateKey      If the user should be prompted for the private key passphrase
407
     * @param array                  $processedReplacements    The processed list of replacement placeholders and their values
408
     * @param null|non-empty-string  $shebang                  The shebang line
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|non-empty-string at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in null|non-empty-string.
Loading history...
409
     * @param SigningAlgorithm       $signingAlgorithm         The PHAR siging algorithm. See \Phar constants
410
     * @param null|string            $stubBannerContents       The stub banner comment
411
     * @param null|string            $stubBannerPath           The path to the stub banner comment file
412
     * @param null|string            $stubPath                 The PHAR stub file path
413
     * @param bool                   $isInterceptFileFuncs     Whether Phar::interceptFileFuncs() should be used
414
     * @param bool                   $isStubGenerated          Whether if the PHAR stub should be generated
415
     * @param null|DateTimeImmutable $timestamp                Timestamp at which the PHAR will be set to.
416
     * @param bool                   $checkRequirements        Whether the PHAR will check the application requirements before
417
     *                                                         running
418
     * @param string[]               $warnings
419
     * @param string[]               $recommendations
420
     */
421
    private function __construct(
422
        private readonly ?string $file,
423
        private readonly string $alias,
424
        private readonly string $basePath,
425
        private readonly ?ComposerJson $composerJson,
426
        private readonly ?ComposerLock $composerLock,
427
        private readonly array $files,
428
        private readonly array $binaryFiles,
429
        private readonly bool $autodiscoveredFiles,
430
        private readonly bool $dumpAutoload,
431
        private readonly bool $excludeComposerArtifacts,
432
        private readonly bool $excludeDevFiles,
433
        private readonly array|Compactors $compactors,
434
        private readonly CompressionAlgorithm $compressionAlgorithm,
435
        private readonly null|int|string $fileMode,
436
        ?string $mainScriptPath,
437
        ?string $mainScriptContents,
438
        private readonly MapFile $fileMapper,
439
        private readonly mixed $metadata,
440
        private readonly string $tmpOutputPath,
441
        private readonly string $outputPath,
442
        private readonly ?string $privateKeyPassphrase,
443
        private readonly ?string $privateKeyPath,
444
        private readonly bool $promptForPrivateKey,
445
        private readonly array $processedReplacements,
446
        private readonly ?string $shebang,
447
        private readonly SigningAlgorithm $signingAlgorithm,
448
        private readonly ?string $stubBannerContents,
449
        private readonly ?string $stubBannerPath,
450
        private readonly ?string $stubPath,
451
        private readonly bool $isInterceptFileFuncs,
452
        private readonly bool $isStubGenerated,
453
        private readonly ?DateTimeImmutable $timestamp,
454
        private readonly bool $checkRequirements,
455
        private readonly array $warnings,
456
        private readonly array $recommendations,
457
    ) {
458
        if (null === $mainScriptPath) {
459
            Assert::null($mainScriptContents);
460
        } else {
461
            Assert::notNull($mainScriptContents);
462
        }
463
464
        $this->mainScriptPath = $mainScriptPath;
0 ignored issues
show
Bug introduced by
The property mainScriptPath is declared read-only in KevinGH\Box\Configuration\Configuration.
Loading history...
465
        $this->mainScriptContents = $mainScriptContents;
0 ignored issues
show
Bug introduced by
The property mainScriptContents is declared read-only in KevinGH\Box\Configuration\Configuration.
Loading history...
466
    }
467
468
    public function setComposerBin(?string $composerBin): void
469
    {
470
        $this->composerBin = $composerBin;
471
    }
472
473
    public function getComposerBin(): ?string
474
    {
475
        return $this->composerBin;
476
    }
477
478
    public function export(): string
479
    {
480
        return ExportableConfiguration::create($this)->export();
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\Configuration\ExportableConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
481
    }
482
483
    public function getConfigurationFile(): ?string
484
    {
485
        return $this->file;
486
    }
487
488
    public function getAlias(): string
489
    {
490
        return $this->alias;
491
    }
492
493
    public function getBasePath(): string
494
    {
495
        return $this->basePath;
496
    }
497
498
    public function getComposerJson(): ?ComposerJson
499
    {
500
        return $this->composerJson;
501
    }
502
503
    public function getComposerLock(): ?ComposerLock
504
    {
505
        return $this->composerLock;
506
    }
507
508
    /**
509
     * @return SplFileInfo[]
510
     */
511
    public function getFiles(): array
512
    {
513
        return $this->files;
514
    }
515
516
    /**
517
     * @return SplFileInfo[]
518
     */
519
    public function getBinaryFiles(): array
520
    {
521
        return $this->binaryFiles;
522
    }
523
524
    public function hasAutodiscoveredFiles(): bool
525
    {
526
        return $this->autodiscoveredFiles;
527
    }
528
529
    public function dumpAutoload(): bool
530
    {
531
        return $this->dumpAutoload;
532
    }
533
534
    public function excludeComposerArtifacts(): bool
535
    {
536
        return $this->excludeComposerArtifacts;
537
    }
538
539
    public function excludeDevFiles(): bool
540
    {
541
        return $this->excludeDevFiles;
542
    }
543
544
    public function getCompactors(): Compactors
545
    {
546
        return $this->compactors;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->compactors could return the type array which is incompatible with the type-hinted return KevinGH\Box\Compactor\Compactors. Consider adding an additional type-check to rule them out.
Loading history...
547
    }
548
549
    public function getCompressionAlgorithm(): CompressionAlgorithm
550
    {
551
        return $this->compressionAlgorithm;
552
    }
553
554
    public function getFileMode(): ?int
555
    {
556
        return $this->fileMode;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fileMode could return the type string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
557
    }
558
559
    public function hasMainScript(): bool
560
    {
561
        return null !== $this->mainScriptPath;
562
    }
563
564
    public function getMainScriptPath(): string
565
    {
566
        Assert::notNull(
567
            $this->mainScriptPath,
568
            'Cannot retrieve the main script path: no main script configured.',
569
        );
570
571
        return $this->mainScriptPath;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mainScriptPath could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
572
    }
573
574
    public function getMainScriptContents(): string
575
    {
576
        Assert::notNull(
577
            $this->mainScriptPath,
578
            'Cannot retrieve the main script contents: no main script configured.',
579
        );
580
581
        return $this->mainScriptContents;
582
    }
583
584
    public function checkRequirements(): bool
585
    {
586
        return $this->checkRequirements;
587
    }
588
589
    public function getTmpOutputPath(): string
590
    {
591
        return $this->tmpOutputPath;
592
    }
593
594
    public function getOutputPath(): string
595
    {
596
        return $this->outputPath;
597
    }
598
599
    public function getFileMapper(): MapFile
600
    {
601
        return $this->fileMapper;
602
    }
603
604
    public function getMetadata(): mixed
605
    {
606
        return $this->metadata;
607
    }
608
609
    public function getPrivateKeyPassphrase(): ?string
610
    {
611
        return $this->privateKeyPassphrase;
612
    }
613
614
    public function getPrivateKeyPath(): ?string
615
    {
616
        return $this->privateKeyPath;
617
    }
618
619
    /**
620
     * @deprecated Use promptForPrivateKey() instead
621
     */
622
    public function isPrivateKeyPrompt(): bool
623
    {
624
        return $this->promptForPrivateKey;
625
    }
626
627
    public function promptForPrivateKey(): bool
628
    {
629
        return $this->promptForPrivateKey;
630
    }
631
632
    /**
633
     * @return scalar[]
634
     */
635
    public function getReplacements(): array
636
    {
637
        return $this->processedReplacements;
638
    }
639
640
    public function getShebang(): ?string
641
    {
642
        return $this->shebang;
643
    }
644
645
    public function getSigningAlgorithm(): SigningAlgorithm
646
    {
647
        return $this->signingAlgorithm;
648
    }
649
650
    public function getStubBannerContents(): ?string
651
    {
652
        return $this->stubBannerContents;
653
    }
654
655
    public function getStubBannerPath(): ?string
656
    {
657
        return $this->stubBannerPath;
658
    }
659
660
    public function getStubPath(): ?string
661
    {
662
        return $this->stubPath;
663
    }
664
665
    public function isInterceptFileFuncs(): bool
666
    {
667
        return $this->isInterceptFileFuncs;
668
    }
669
670
    public function isStubGenerated(): bool
671
    {
672
        return $this->isStubGenerated;
673
    }
674
675
    public function getTimestamp(): ?DateTimeImmutable
676
    {
677
        return $this->timestamp;
678
    }
679
680
    /**
681
     * @return string[]
682
     */
683
    public function getWarnings(): array
684
    {
685
        return $this->warnings;
686
    }
687
688
    /**
689
     * @return string[]
690
     */
691
    public function getRecommendations(): array
692
    {
693
        return $this->recommendations;
694
    }
695
696
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
697
    {
698
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
699
700
        if (false === isset($raw->{self::ALIAS_KEY})) {
701
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
702
        }
703
704
        $alias = trim($raw->{self::ALIAS_KEY});
705
706
        Assert::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
707
708
        if ($userStubUsed) {
709
            $logger->addWarning(
710
                sprintf(
711
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
712
                    self::ALIAS_KEY,
713
                ),
714
            );
715
        }
716
717
        return $alias;
718
    }
719
720
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
721
    {
722
        if (null === $file) {
723
            return getcwd();
724
        }
725
726
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
727
            return realpath(dirname($file));
728
        }
729
730
        $basePath = trim($raw->{self::BASE_PATH_KEY});
731
732
        Assert::directory(
733
            $basePath,
734
            'The base path %s is not a directory or does not exist.',
735
        );
736
737
        $basePath = realpath($basePath);
738
        $defaultPath = realpath(dirname($file));
739
740
        if ($basePath === $defaultPath) {
741
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
742
        }
743
744
        return $basePath;
745
    }
746
747
    /**
748
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
749
     */
750
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
751
    {
752
        if (null === $file) {
753
            return true;
754
        }
755
756
        $associativeRaw = (array) $raw;
757
758
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
759
    }
760
761
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
762
    {
763
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
764
765
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
766
    }
767
768
    private static function retrieveBlacklistFilter(
769
        stdClass $raw,
770
        string $basePath,
771
        ConfigurationLogger $logger,
772
        ?string ...$excludedPaths,
773
    ): array {
774
        $blacklist = array_flip(
775
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths),
776
        );
777
778
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
779
            if ($file->isLink()) {
780
                return false;
781
            }
782
783
            if (false === $file->getRealPath()) {
784
                return false;
785
            }
786
787
            if (array_key_exists($file->getRealPath(), $blacklist)) {
788
                return false;
789
            }
790
791
            return null;
792
        };
793
794
        return [array_keys($blacklist), $blacklistFilter];
795
    }
796
797
    /**
798
     * @param null[]|string[] $excludedPaths
799
     *
800
     * @return string[]
801
     */
802
    private static function retrieveBlacklist(
803
        stdClass $raw,
804
        string $basePath,
805
        ConfigurationLogger $logger,
806
        ?string ...$excludedPaths,
807
    ): array {
808
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
809
810
        $normalizedBlacklist = array_map(
811
            static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath),
812
            array_filter($excludedPaths),
813
        );
814
815
        /** @var string[] $blacklist */
816
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
817
818
        foreach ($blacklist as $file) {
819
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
820
            $normalizedBlacklist[] = Path::canonicalize(Path::makeRelative(trim($file), $basePath));
821
        }
822
823
        return array_unique($normalizedBlacklist);
824
    }
825
826
    /**
827
     * @param string[] $excludedPaths
828
     * @param string[] $alwaysExcludedPaths
829
     * @param string[] $devPackages
830
     *
831
     * @return SplFileInfo[]
832
     */
833
    private static function collectFiles(
834
        stdClass $raw,
835
        string $basePath,
836
        ?string $mainScriptPath,
837
        Closure $blacklistFilter,
838
        array $excludedPaths,
839
        array $alwaysExcludedPaths,
840
        array $devPackages,
841
        ComposerArtifacts $composerArtifacts,
842
        bool $autodiscoverFiles,
843
        bool $forceFilesAutodiscovery,
844
        ConfigurationLogger $logger,
845
    ): array {
846
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerArtifacts, $alwaysExcludedPaths, $logger)];
847
848
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
849
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
850
                $basePath,
851
                $composerArtifacts->composerJson,
852
                $devPackages,
853
                $composerArtifacts->getPaths(),
854
                $excludedPaths,
855
            );
856
857
            $files[] = self::wrapInSplFileInfo($filesToAppend);
858
859
            $files[] = self::retrieveAllFiles(
860
                $basePath,
861
                $directories,
862
                $mainScriptPath,
863
                $blacklistFilter,
864
                $excludedPaths,
865
                $devPackages,
866
            );
867
        }
868
869
        if (false === $autodiscoverFiles) {
870
            $files[] = self::retrieveDirectories(
871
                $raw,
872
                self::DIRECTORIES_KEY,
873
                $basePath,
874
                $blacklistFilter,
875
                $excludedPaths,
876
                $logger,
877
            );
878
879
            $filesFromFinders = self::retrieveFilesFromFinders(
880
                $raw,
881
                self::FINDER_KEY,
882
                $basePath,
883
                $blacklistFilter,
884
                $devPackages,
885
                $logger,
886
            );
887
888
            foreach ($filesFromFinders as $filesFromFinder) {
889
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
890
                $files[] = $filesFromFinder;
891
            }
892
893
            $files[] = self::wrapInSplFileInfo($composerArtifacts->getPaths());
894
        }
895
896
        return self::retrieveFilesAggregate(...$files);
897
    }
898
899
    /**
900
     * @param string[] $excludedPaths
901
     * @param string[] $alwaysExcludedPaths
902
     * @param string[] $devPackages
903
     *
904
     * @return SplFileInfo[]
905
     */
906
    private static function collectBinaryFiles(
907
        stdClass $raw,
908
        string $basePath,
909
        Closure $blacklistFilter,
910
        array $excludedPaths,
911
        array $alwaysExcludedPaths,
912
        array $devPackages,
913
        ConfigurationLogger $logger,
914
    ): array {
915
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, new ComposerArtifacts(), $alwaysExcludedPaths, $logger);
916
917
        $binaryDirectories = self::retrieveDirectories(
918
            $raw,
919
            self::DIRECTORIES_BIN_KEY,
920
            $basePath,
921
            $blacklistFilter,
922
            $excludedPaths,
923
            $logger,
924
        );
925
926
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
927
            $raw,
928
            self::FINDER_BIN_KEY,
929
            $basePath,
930
            $blacklistFilter,
931
            $devPackages,
932
            $logger,
933
        );
934
935
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
936
    }
937
938
    /**
939
     * @param string[] $excludedFiles
940
     *
941
     * @return SplFileInfo[]
942
     */
943
    private static function retrieveFiles(
944
        stdClass $raw,
945
        string $key,
946
        string $basePath,
947
        ComposerArtifacts $composerArtifacts,
948
        array $excludedFiles,
949
        ConfigurationLogger $logger,
950
    ): array {
951
        self::checkIfDefaultValue($logger, $raw, $key, []);
952
953
        $excludedFiles = array_flip($excludedFiles);
954
        $files = array_filter([
955
            $composerArtifacts->composerJson?->path,
956
            $composerArtifacts->composerLock?->path,
957
        ]);
958
959
        if (false === isset($raw->{$key})) {
960
            return self::wrapInSplFileInfo($files);
961
        }
962
963
        if ([] === (array) $raw->{$key}) {
964
            return self::wrapInSplFileInfo($files);
965
        }
966
967
        $files = array_merge((array) $raw->{$key}, $files);
968
969
        Assert::allString($files);
970
971
        $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo {
972
            $file = self::normalizePath($file, $basePath);
973
974
            Assert::false(
975
                is_link($file),
976
                sprintf(
977
                    'Cannot add the link "%s": links are not supported.',
978
                    $file,
979
                ),
980
            );
981
982
            Assert::file(
983
                $file,
984
                sprintf(
985
                    '"%s" must contain a list of existing files. Could not find %%s.',
986
                    $key,
987
                ),
988
            );
989
990
            return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file);
991
        };
992
993
        return array_filter(array_map($normalizePath, $files));
994
    }
995
996
    /**
997
     * @param string   $key           Config property name
998
     * @param string[] $excludedPaths
999
     *
1000
     * @return iterable&(SplFileInfo[]&Finder)
1001
     */
1002
    private static function retrieveDirectories(
1003
        stdClass $raw,
1004
        string $key,
1005
        string $basePath,
1006
        Closure $blacklistFilter,
1007
        array $excludedPaths,
1008
        ConfigurationLogger $logger,
1009
    ): iterable {
1010
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1011
1012
        if ([] !== $directories) {
1013
            $finder = Finder::create()
1014
                ->files()
1015
                ->filter($blacklistFilter)
1016
                ->ignoreVCS(true)
1017
                ->in($directories);
1018
1019
            foreach ($excludedPaths as $excludedPath) {
1020
                $finder->notPath($excludedPath);
1021
            }
1022
1023
            return $finder;
1024
        }
1025
1026
        return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type Symfony\Component\Finder\Finder.
Loading history...
1027
    }
1028
1029
    /**
1030
     * @param string[] $devPackages
1031
     *
1032
     * @return iterable[]|SplFileInfo[][]
1033
     */
1034
    private static function retrieveFilesFromFinders(
1035
        stdClass $raw,
1036
        string $key,
1037
        string $basePath,
1038
        Closure $blacklistFilter,
1039
        array $devPackages,
1040
        ConfigurationLogger $logger,
1041
    ): array {
1042
        self::checkIfDefaultValue($logger, $raw, $key, []);
1043
1044
        if (false === isset($raw->{$key})) {
1045
            return [];
1046
        }
1047
1048
        $finder = $raw->{$key};
1049
1050
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1051
    }
1052
1053
    /**
1054
     * @param iterable[]|SplFileInfo[][] $fileIterators
1055
     *
1056
     * @return SplFileInfo[]
1057
     */
1058
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1059
    {
1060
        $files = [];
1061
1062
        foreach ($fileIterators as $fileIterator) {
1063
            foreach ($fileIterator as $file) {
1064
                $files[(string) $file] = $file;
1065
            }
1066
        }
1067
1068
        return array_values($files);
1069
    }
1070
1071
    /**
1072
     * @param string[] $devPackages
1073
     *
1074
     * @return Finder[]|SplFileInfo[][]
1075
     */
1076
    private static function processFinders(
1077
        array $findersConfig,
1078
        string $basePath,
1079
        Closure $blacklistFilter,
1080
        array $devPackages,
1081
    ): array {
1082
        $processFinderConfig = static fn (stdClass $config) => self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1083
1084
        return array_map($processFinderConfig, $findersConfig);
1085
    }
1086
1087
    /**
1088
     * @param string[] $devPackages
1089
     *
1090
     * @return Finder|SplFileInfo[]
1091
     */
1092
    private static function processFinder(
1093
        stdClass $config,
1094
        string $basePath,
1095
        Closure $blacklistFilter,
1096
        array $devPackages,
1097
    ): Finder {
1098
        $finder = Finder::create()
1099
            ->files()
1100
            ->filter($blacklistFilter)
1101
            ->filter(
1102
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1103
                    foreach ($devPackages as $devPackage) {
1104
                        if ($devPackage === Path::getLongestCommonBasePath($devPackage, $fileInfo->getRealPath())) {
1105
                            // File belongs to the dev package
1106
                            return false;
1107
                        }
1108
                    }
1109
1110
                    return true;
1111
                },
1112
            )
1113
            ->ignoreVCS(true);
1114
1115
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1116
            $normalizedConfig = [];
1117
1118
            foreach ($config as $method => $arguments) {
1119
                $method = trim($method);
1120
                $arguments = (array) $arguments;
1121
1122
                Assert::methodExists($finder, $method);
1123
1124
                $normalizedConfig[$method] = $arguments;
1125
            }
1126
1127
            krsort($normalizedConfig);
1128
1129
            return $normalizedConfig;
1130
        })((array) $config, $finder);
1131
1132
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1133
            $directory = self::normalizePath($directory, $basePath);
1134
1135
            Assert::false(
1136
                is_link($directory),
1137
                sprintf(
1138
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1139
                    $directory,
1140
                ),
1141
            );
1142
1143
            Assert::directory($directory);
1144
1145
            return $directory;
1146
        };
1147
1148
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1149
            if (null === $fileOrDirectory) {
1150
                return;
1151
            }
1152
1153
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
1154
1155
            Assert::false(
1156
                is_link($fileOrDirectory),
1157
                sprintf(
1158
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1159
                    $fileOrDirectory,
1160
                ),
1161
            );
1162
1163
            Assert::true(
1164
                file_exists($fileOrDirectory),
1165
                sprintf(
1166
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1167
                    $fileOrDirectory,
1168
                ),
1169
            );
1170
1171
            if (false === is_file($fileOrDirectory)) {
1172
                Assert::directory($fileOrDirectory);
1173
            } else {
1174
                Assert::file($fileOrDirectory);
1175
            }
1176
1177
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1178
                $fileOrDirectory = null;
1179
            }
1180
        };
1181
1182
        foreach ($normalizedConfig as $method => $arguments) {
1183
            if ('in' === $method) {
1184
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1185
            }
1186
1187
            if ('exclude' === $method) {
1188
                $arguments = array_unique(array_map('trim', $arguments));
1189
            }
1190
1191
            if ('append' === $method) {
1192
                array_walk($arguments, $normalizeFileOrDirectory);
1193
1194
                $arguments = [array_filter($arguments)];
1195
            }
1196
1197
            foreach ($arguments as $argument) {
1198
                $finder->{$method}($argument);
1199
            }
1200
        }
1201
1202
        return $finder;
1203
    }
1204
1205
    /**
1206
     * @param string[] $devPackages
1207
     * @param string[] $filesToAppend
1208
     *
1209
     * @return string[][]
1210
     */
1211
    private static function retrieveAllDirectoriesToInclude(
1212
        string $basePath,
1213
        ?ComposerJson $composerJson,
1214
        array $devPackages,
1215
        array $filesToAppend,
1216
        array $excludedPaths,
1217
    ): array {
1218
        $toString = static fn (SplFileInfo|string $file): string => (string) $file;
1219
1220
        $vendorDir = self::normalizePath(
1221
            ComposerConfiguration::retrieveVendorDir($composerJson),
1222
            $basePath,
1223
        );
1224
1225
        if (file_exists($vendorDir)) {
1226
            // Note that some files may not exist. For example installed.json does not exist at all if no dependencies
1227
            // are included in composer.json.
1228
            $requiredComposerArtifacts = [
1229
                'installed.json',
1230
                'installed.php',
1231
                'InstalledVersions.php',
1232
            ];
1233
1234
            foreach ($requiredComposerArtifacts as $requiredComposerArtifact) {
1235
                $normalizePath = self::normalizePath($vendorDir.'/composer/'.$requiredComposerArtifact, $basePath);
1236
1237
                if (file_exists($normalizePath)) {
1238
                    $filesToAppend[] = $normalizePath;
1239
                }
1240
            }
1241
1242
            $vendorPackages = toArray(values(map(
1243
                $toString,
1244
                Finder::create()
1245
                    ->in($vendorDir)
1246
                    ->directories()
1247
                    ->depth(1)
1248
                    ->ignoreUnreadableDirs()
1249
                    ->filter(
1250
                        static function (SplFileInfo $fileInfo): ?bool {
1251
                            if ($fileInfo->isLink()) {
1252
                                return false;
1253
                            }
1254
1255
                            return null;
1256
                        },
1257
                    ),
1258
            )));
1259
1260
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1261
1262
            if (!($composerJson?->hasAutoload() ?? false)) {
1263
                $files = toArray(values(map(
1264
                    $toString,
1265
                    Finder::create()
1266
                        ->in($basePath)
1267
                        ->files()
1268
                        ->depth(0),
1269
                )));
1270
1271
                $directories = toArray(values(map(
1272
                    $toString,
1273
                    Finder::create()
1274
                        ->in($basePath)
1275
                        ->notPath('vendor')
1276
                        ->directories()
1277
                        ->depth(0),
1278
                )));
1279
1280
                return [
1281
                    array_merge(
1282
                        array_diff($files, $excludedPaths),
1283
                        $filesToAppend,
1284
                    ),
1285
                    array_merge(
1286
                        array_diff($directories, $excludedPaths),
1287
                        $vendorPackages,
1288
                    ),
1289
                ];
1290
            }
1291
1292
            $paths = $vendorPackages;
1293
        } else {
1294
            $paths = [];
1295
        }
1296
1297
        $paths = array_merge(
1298
            $paths,
1299
            $composerJson?->getAutoloadPaths() ?? [],
1300
        );
1301
1302
        $normalizePath = static fn (string $path): string => Path::isAbsolute($path)
1303
            ? Path::canonicalize($path)
1304
            : self::normalizePath(trim($path, '/ '), $basePath);
1305
1306
        $composerFiles = $composerJson?->getAutoloadFiles() ?? [];
1307
        foreach ($composerFiles as $path) {
1308
            /** @var string $path */
1309
            $path = $normalizePath($path);
1310
1311
            Assert::file($path);
1312
            Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1313
1314
            $filesToAppend[] = $path;
1315
        }
1316
1317
        $files = $filesToAppend;
1318
        $directories = [];
1319
1320
        foreach ($paths as $path) {
1321
            $path = $normalizePath($path);
1322
1323
            Assert::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1324
            Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1325
1326
            if (is_file($path)) {
1327
                $files[] = $path;
1328
            } else {
1329
                $directories[] = $path;
1330
            }
1331
        }
1332
1333
        [$files, $directories] = [
1334
            array_unique($files),
1335
            array_unique($directories),
1336
        ];
1337
1338
        return [
1339
            array_diff($files, $excludedPaths),
1340
            array_diff($directories, $excludedPaths),
1341
        ];
1342
    }
1343
1344
    /**
1345
     * @param string[] $directories
1346
     * @param string[] $excludedPaths
1347
     * @param string[] $devPackages
1348
     *
1349
     * @return Finder|SplFileInfo[]
1350
     */
1351
    private static function retrieveAllFiles(
1352
        string $basePath,
1353
        array $directories,
1354
        ?string $mainScriptPath,
1355
        Closure $blacklistFilter,
1356
        array $excludedPaths,
1357
        array $devPackages,
1358
    ): iterable {
1359
        if ([] === $directories) {
1360
            return [];
1361
        }
1362
1363
        $relativeDevPackages = array_map(
1364
            static fn (string $packagePath): string => Path::makeRelative($packagePath, $basePath),
1365
            $devPackages,
1366
        );
1367
1368
        $finder = Finder::create()
1369
            ->files()
1370
            ->filter($blacklistFilter)
1371
            ->exclude($relativeDevPackages)
1372
            ->ignoreVCS(true)
1373
            ->ignoreDotFiles(true)
1374
            // Remove build files
1375
            ->notName('composer.json')
1376
            ->notName('composer.lock')
1377
            ->notName('Makefile')
1378
            ->notName('Vagrantfile')
1379
            ->notName('phpstan*.neon*')
1380
            ->notName('infection*.json*')
1381
            ->notName('humbug*.json*')
1382
            ->notName('easy-coding-standard.neon*')
1383
            ->notName('phpbench.json*')
1384
            ->notName('phpcs.xml*')
1385
            ->notName('psalm.xml*')
1386
            ->notName('scoper.inc*')
1387
            ->notName('box*.json*')
1388
            ->notName('phpdoc*.xml*')
1389
            ->notName('codecov.yml*')
1390
            ->notName('Dockerfile')
1391
            ->exclude('build')
1392
            ->exclude('dist')
1393
            ->exclude('example')
1394
            ->exclude('examples')
1395
            // Remove documentation
1396
            ->notName('*.md')
1397
            ->notName('*.rst')
1398
            ->notName('/^readme((?!\.php)(\..*+))?$/i')
1399
            ->notName('/^upgrade((?!\.php)(\..*+))?$/i')
1400
            ->notName('/^contributing((?!\.php)(\..*+))?$/i')
1401
            ->notName('/^changelog((?!\.php)(\..*+))?$/i')
1402
            ->notName('/^authors?((?!\.php)(\..*+))?$/i')
1403
            ->notName('/^conduct((?!\.php)(\..*+))?$/i')
1404
            ->notName('/^todo((?!\.php)(\..*+))?$/i')
1405
            ->exclude('doc')
1406
            ->exclude('docs')
1407
            ->exclude('documentation')
1408
            // Remove backup files
1409
            ->notName('*~')
1410
            ->notName('*.back')
1411
            ->notName('*.swp')
1412
            // Remove tests
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
        if (null !== $mainScriptPath) {
1427
            $finder->notPath(Path::makeRelative($mainScriptPath, $basePath));
1428
        }
1429
1430
        $finder->in($directories);
1431
1432
        $excludedPaths = array_unique(
1433
            array_filter(
1434
                array_map(
1435
                    static fn (string $path): string => Path::makeRelative($path, $basePath),
1436
                    $excludedPaths,
1437
                ),
1438
                static fn (string $path): bool => !str_starts_with($path, '..'),
1439
            ),
1440
        );
1441
1442
        foreach ($excludedPaths as $excludedPath) {
1443
            $finder->notPath($excludedPath);
1444
        }
1445
1446
        return $finder;
1447
    }
1448
1449
    /**
1450
     * @param string $key Config property name
1451
     *
1452
     * @return string[]
1453
     */
1454
    private static function retrieveDirectoryPaths(
1455
        stdClass $raw,
1456
        string $key,
1457
        string $basePath,
1458
        ConfigurationLogger $logger,
1459
    ): array {
1460
        self::checkIfDefaultValue($logger, $raw, $key, []);
1461
1462
        if (false === isset($raw->{$key})) {
1463
            return [];
1464
        }
1465
1466
        $directories = $raw->{$key};
1467
1468
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1469
            $directory = self::normalizePath($directory, $basePath);
1470
1471
            Assert::false(
1472
                is_link($directory),
1473
                sprintf(
1474
                    'Cannot add the link "%s": links are not supported.',
1475
                    $directory,
1476
                ),
1477
            );
1478
1479
            Assert::directory(
1480
                $directory,
1481
                sprintf(
1482
                    '"%s" must contain a list of existing directories. Could not find %%s.',
1483
                    $key,
1484
                ),
1485
            );
1486
1487
            return $directory;
1488
        };
1489
1490
        return array_map($normalizeDirectory, $directories);
1491
    }
1492
1493
    private static function normalizePath(string $file, string $basePath): string
1494
    {
1495
        return Path::makeAbsolute(trim($file), $basePath);
1496
    }
1497
1498
    /**
1499
     * @param string[] $files
1500
     *
1501
     * @return SplFileInfo[]
1502
     */
1503
    private static function wrapInSplFileInfo(array $files): array
1504
    {
1505
        return array_map(
1506
            static fn (string $file): SplFileInfo => new SplFileInfo($file),
1507
            $files,
1508
        );
1509
    }
1510
1511
    private static function retrieveDumpAutoload(stdClass $raw, ComposerArtifacts $composerArtifacts, ConfigurationLogger $logger): bool
1512
    {
1513
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, null);
1514
1515
        $canDumpAutoload = (
1516
            null !== $composerArtifacts->composerJson?->path
1517
            && (
1518
                // The composer.lock and installed.json are optional (e.g. if there is no dependencies installed)
1519
                // but when one is present, the other must be as well otherwise the dumped autoloader will be broken
1520
                (
1521
                    null === $composerArtifacts->composerLock?->path
1522
                    && null === $composerArtifacts->installedJson?->path
1523
                )
1524
                || (
1525
                    null !== $composerArtifacts->composerLock?->path
1526
                    && null !== $composerArtifacts->installedJson?->path
1527
                )
1528
                || (
1529
                    null === $composerArtifacts->composerLock?->path
1530
                    && null !== $composerArtifacts->installedJson?->path
1531
                    && [] === $composerArtifacts->installedJson?->decodedContents
1532
                )
1533
            )
1534
        );
1535
1536
        if ($canDumpAutoload) {
1537
            self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1538
        }
1539
1540
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1541
            return $canDumpAutoload;
1542
        }
1543
1544
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1545
1546
        if (false === $canDumpAutoload && $dumpAutoload) {
1547
            $logger->addWarning(
1548
                sprintf(
1549
                    'The "%s" setting has been set but has been ignored because the composer.json, composer.lock'
1550
                    .' and vendor/composer/installed.json files are necessary but could not be found.',
1551
                    self::DUMP_AUTOLOAD_KEY,
1552
                ),
1553
            );
1554
1555
            return false;
1556
        }
1557
1558
        return $canDumpAutoload && false !== $dumpAutoload;
1559
    }
1560
1561
    private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool
1562
    {
1563
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload);
1564
1565
        if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) {
1566
            return $dumpAutoload;
1567
        }
1568
1569
        $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload;
1570
1571
        if (true === $excludeDevFiles && false === $dumpAutoload) {
1572
            $logger->addWarning(sprintf(
1573
                'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped',
1574
                self::EXCLUDE_DEV_FILES_KEY,
1575
            ));
1576
1577
            return false;
1578
        }
1579
1580
        return $excludeDevFiles;
1581
    }
1582
1583
    private static function retrieveExcludeComposerArtifacts(stdClass $raw, ConfigurationLogger $logger): bool
1584
    {
1585
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1586
1587
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1588
    }
1589
1590
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactors
1591
    {
1592
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1593
1594
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1595
1596
        // Needs to do this check before returning the compactors in order to properly inform the users about
1597
        // possible misconfiguration
1598
        $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger);
1599
1600
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1601
            return new Compactors();
1602
        }
1603
1604
        $compactors = new Compactors(
1605
            ...self::createCompactors(
1606
                $raw,
1607
                $basePath,
1608
                $compactorClasses,
1609
                $ignoredAnnotations,
1610
                $logger,
1611
            ),
1612
        );
1613
1614
        self::checkCompactorsOrder($logger, $compactors);
1615
1616
        return $compactors;
1617
    }
1618
1619
    /**
1620
     * @param string[]      $compactorClasses
1621
     * @param string[]|null $ignoredAnnotations
1622
     *
1623
     * @return Compactor[]
1624
     */
1625
    private static function createCompactors(
1626
        stdClass $raw,
1627
        string $basePath,
1628
        array $compactorClasses,
1629
        ?array $ignoredAnnotations,
1630
        ConfigurationLogger $logger,
1631
    ): array {
1632
        return array_map(
1633
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1634
                Assert::classExists($class, 'The compactor class %s does not exist.');
1635
                Assert::isAOf($class, Compactor::class, sprintf('The class "%s" is not a compactor class.', $class));
1636
1637
                if (in_array($class, [PhpCompactor::class, 'KevinGH\Box\Compactor\Php'], true)) {
1638
                    return self::createPhpCompactor($ignoredAnnotations);
1639
                }
1640
1641
                if (in_array($class, [PhpScoperCompactor::class, 'KevinGH\Box\Compactor\PhpScoper'], true)) {
1642
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1643
                }
1644
1645
                return new $class();
1646
            },
1647
            $compactorClasses,
1648
        );
1649
    }
1650
1651
    private static function checkCompactorsOrder(ConfigurationLogger $logger, Compactors $compactors): void
1652
    {
1653
        $scoperCompactor = false;
1654
1655
        foreach ($compactors->toArray() as $compactor) {
1656
            if ($compactor instanceof PhpScoperCompactor) {
1657
                $scoperCompactor = true;
1658
            }
1659
1660
            if ($compactor instanceof PhpCompactor) {
1661
                if (true === $scoperCompactor) {
1662
                    $logger->addRecommendation(
1663
                        'The PHP compactor has been registered after the PhpScoper compactor. It is '
1664
                        .'recommended to register the PHP compactor before for a clearer code and faster processing.',
1665
                    );
1666
                }
1667
1668
                break;
1669
            }
1670
        }
1671
    }
1672
1673
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): CompressionAlgorithm
1674
    {
1675
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1676
1677
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1678
            return CompressionAlgorithm::NONE;
1679
        }
1680
1681
        $knownAlgorithms = CompressionAlgorithm::getLabels();
1682
1683
        Assert::nullOrInArray(
1684
            $raw->{self::COMPRESSION_KEY},
1685
            $knownAlgorithms,
1686
            sprintf(
1687
                'Unknown compression algorithm %%s. Expected one of "%s".',
1688
                implode('", "', $knownAlgorithms),
1689
            ),
1690
        );
1691
1692
        return CompressionAlgorithm::fromLabel($raw->{self::COMPRESSION_KEY});
1693
    }
1694
1695
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1696
    {
1697
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1698
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1699
        }
1700
1701
        $defaultChmod = intval(0o755, 8);
1702
1703
        if (isset($raw->{self::CHMOD_KEY})) {
1704
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1705
1706
            if ($defaultChmod === $chmod) {
1707
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1708
            }
1709
1710
            return $chmod;
1711
        }
1712
1713
        return $defaultChmod;
1714
    }
1715
1716
    private static function retrieveMainScriptPath(
1717
        stdClass $raw,
1718
        string $basePath,
1719
        ?ComposerJson $composerJson,
1720
        ConfigurationLogger $logger,
1721
    ): ?string {
1722
        $firstBin = $composerJson?->getFirstBin();
1723
1724
        if (null !== $firstBin) {
1725
            $firstBin = self::normalizePath($firstBin, $basePath);
1726
        }
1727
1728
        if (isset($raw->{self::MAIN_KEY})) {
1729
            $main = $raw->{self::MAIN_KEY};
1730
1731
            if (is_string($main)) {
1732
                $main = self::normalizePath($main, $basePath);
1733
1734
                if ($main === $firstBin) {
1735
                    $logger->addRecommendation(
1736
                        sprintf(
1737
                            'The "%s" setting can be omitted since is set to its default value',
1738
                            self::MAIN_KEY,
1739
                        ),
1740
                    );
1741
                }
1742
            }
1743
        } else {
1744
            $main = $firstBin ?? self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1745
        }
1746
1747
        if (is_bool($main)) {
1748
            Assert::false(
1749
                $main,
1750
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.',
1751
            );
1752
1753
            return null;
1754
        }
1755
1756
        Assert::file($main);
1757
1758
        return $main;
1759
    }
1760
1761
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1762
    {
1763
        if (null === $mainScriptPath) {
1764
            return null;
1765
        }
1766
1767
        $contents = FS::getFileContents($mainScriptPath);
1768
1769
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1770
        // PHAR entry point file.
1771
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1772
        // case the main script can be ignored or disabled.
1773
        return preg_replace('/^#!.*\s*/', '', $contents);
1774
    }
1775
1776
    private static function retrieveComposerArtifacts(string $basePath): ComposerArtifacts
1777
    {
1778
        $composerJson = self::retrieveComposerArtifact(Path::canonicalize($basePath.'/composer.json'));
1779
        $composerLock = self::retrieveComposerArtifact(Path::canonicalize($basePath.'/composer.lock'));
1780
1781
        return new ComposerArtifacts(
1782
            $composerJson?->toComposerJson(),
1783
            $composerLock?->toComposerLock(),
1784
            self::retrieveComposerArtifact(Path::canonicalize($basePath.'/vendor/composer/installed.json')),
1785
        );
1786
    }
1787
1788
    private static function retrieveComposerArtifact(string $path): ?ComposerArtifact
1789
    {
1790
        if (false === file_exists($path) || false === is_file($path) || false === is_readable($path)) {
1791
            return null;
1792
        }
1793
1794
        try {
1795
            $contents = json_decode(
1796
                FS::getFileContents($path),
1797
                true,
1798
            );
1799
        } catch (JsonException $exception) {
1800
            throw new InvalidArgumentException(
1801
                sprintf(
1802
                    'Expected the file "%s" to be a valid JSON file but an error has been found: %s',
1803
                    $path,
1804
                    $exception->getMessage(),
1805
                ),
1806
                0,
1807
                $exception,
1808
            );
1809
        }
1810
1811
        return new ComposerArtifact($path, $contents);
1812
    }
1813
1814
    /**
1815
     * @return string[][]
1816
     */
1817
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1818
    {
1819
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1820
1821
        if (false === isset($raw->{self::MAP_KEY})) {
1822
            return [];
1823
        }
1824
1825
        $map = [];
1826
1827
        foreach ((array) $raw->{self::MAP_KEY} as $item) {
1828
            $processed = [];
1829
1830
            foreach ($item as $match => $replace) {
1831
                $processed[Path::canonicalize(trim($match))] = Path::canonicalize(trim($replace));
1832
            }
1833
1834
            if (isset($processed['_empty_'])) {
1835
                $processed[''] = $processed['_empty_'];
1836
1837
                unset($processed['_empty_']);
1838
            }
1839
1840
            $map[] = $processed;
1841
        }
1842
1843
        return $map;
1844
    }
1845
1846
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1847
    {
1848
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1849
1850
        if (false === isset($raw->{self::METADATA_KEY})) {
1851
            return null;
1852
        }
1853
1854
        $logger->addWarning('Using the "metadata" setting is deprecated and will be removed in 5.0.0.');
1855
1856
        $metadata = $raw->{self::METADATA_KEY};
1857
1858
        return is_object($metadata) ? (array) $metadata : $metadata;
1859
    }
1860
1861
    /**
1862
     * @return string[] The first element is the temporary output path and the second the final one
1863
     */
1864
    private static function retrieveOutputPath(
1865
        stdClass $raw,
1866
        string $basePath,
1867
        ?string $mainScriptPath,
1868
        ConfigurationLogger $logger,
1869
    ): array {
1870
        $defaultPath = null;
1871
1872
        if (null !== $mainScriptPath
1873
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
1874
        ) {
1875
            $defaultPath = $matches['main'].'.phar';
1876
        }
1877
1878
        if (isset($raw->{self::OUTPUT_KEY})) {
1879
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1880
1881
            if ($path === $defaultPath) {
1882
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1883
            }
1884
        } elseif (null !== $defaultPath) {
1885
            $path = $defaultPath;
1886
        } else {
1887
            // Last resort, should not happen
1888
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1889
        }
1890
1891
        $tmp = $real = $path;
1892
1893
        if (!str_ends_with($real, '.phar')) {
1894
            $tmp .= '.phar';
1895
        }
1896
1897
        return [$tmp, $real];
1898
    }
1899
1900
    private static function retrievePrivateKeyPath(
1901
        stdClass $raw,
1902
        string $basePath,
1903
        SigningAlgorithm $signingAlgorithm,
1904
        ConfigurationLogger $logger,
1905
    ): ?string {
1906
        if (property_exists($raw, self::KEY_KEY) && SigningAlgorithm::OPENSSL !== $signingAlgorithm) {
1907
            if (null === $raw->{self::KEY_KEY}) {
1908
                $logger->addRecommendation(
1909
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".',
1910
                );
1911
            } else {
1912
                $logger->addWarning(
1913
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".',
1914
                );
1915
            }
1916
1917
            return null;
1918
        }
1919
1920
        if (!isset($raw->{self::KEY_KEY})) {
1921
            Assert::true(
1922
                SigningAlgorithm::OPENSSL !== $signingAlgorithm,
1923
                'Expected to have a private key for OpenSSL signing but none have been provided.',
1924
            );
1925
1926
            return null;
1927
        }
1928
1929
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1930
1931
        Assert::file($path);
1932
1933
        return $path;
1934
    }
1935
1936
    private static function retrievePrivateKeyPassphrase(
1937
        stdClass $raw,
1938
        SigningAlgorithm $algorithm,
1939
        ConfigurationLogger $logger,
1940
    ): ?string {
1941
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1942
1943
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1944
            return null;
1945
        }
1946
1947
        /** @var null|false|string $keyPass */
1948
        $keyPass = $raw->{self::KEY_PASS_KEY};
1949
1950
        if (SigningAlgorithm::OPENSSL !== $algorithm) {
1951
            if (false === $keyPass || null === $keyPass) {
1952
                $logger->addRecommendation(
1953
                    sprintf(
1954
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1955
                        .'not "OPENSSL".',
1956
                        self::KEY_PASS_KEY,
1957
                    ),
1958
                );
1959
            } else {
1960
                $logger->addWarning(
1961
                    sprintf(
1962
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1963
                        self::KEY_PASS_KEY,
1964
                    ),
1965
                );
1966
            }
1967
1968
            return null;
1969
        }
1970
1971
        return is_string($keyPass) ? $keyPass : null;
1972
    }
1973
1974
    /**
1975
     * @return scalar[]
1976
     */
1977
    private static function retrieveReplacements(
1978
        stdClass $raw,
1979
        ?string $file,
1980
        string $path,
1981
        ConfigurationLogger $logger,
1982
    ): array {
1983
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1984
1985
        if (null === $file) {
1986
            return [];
1987
        }
1988
1989
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1990
1991
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1992
            $replacements[$git] = self::retrievePrettyGitTag($path);
1993
        }
1994
1995
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1996
            $replacements[$git] = self::retrieveGitHash($path);
1997
        }
1998
1999
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
2000
            $replacements[$git] = self::retrieveGitHash($path, true);
2001
        }
2002
2003
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
2004
            $replacements[$git] = self::retrieveGitTag($path);
2005
        }
2006
2007
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
2008
            $replacements[$git] = self::retrieveGitVersion($path);
2009
        }
2010
2011
        /**
2012
         * @var string $datetimeFormat
2013
         * @var bool   $valueSetByUser
2014
         */
2015
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2016
2017
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2018
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2019
        } elseif ($valueSetByUser) {
2020
            $logger->addRecommendation(
2021
                sprintf(
2022
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2023
                    self::DATETIME_FORMAT_KEY,
2024
                    self::DATETIME_KEY,
2025
                ),
2026
            );
2027
        }
2028
2029
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2030
2031
        foreach ($replacements as $key => $value) {
2032
            unset($replacements[$key]);
2033
            $replacements[$sigil.$key.$sigil] = $value;
2034
        }
2035
2036
        return $replacements;
2037
    }
2038
2039
    private static function retrieveTimestamp(
2040
        stdClass $raw,
2041
        SigningAlgorithm $signingAlgorithm,
2042
        ConfigurationLogger $logger,
2043
    ): ?DateTimeImmutable {
2044
        self::checkIfDefaultValue($logger, $raw, self::TIMESTAMP);
2045
2046
        $timestamp = $raw->{self::TIMESTAMP} ?? null;
2047
2048
        if (null === $timestamp) {
2049
            return null;
2050
        }
2051
2052
        if (SigningAlgorithm::OPENSSL === $signingAlgorithm) {
2053
            $logger->addWarning(
2054
                sprintf(
2055
                    'The "%s" setting has been set but has been ignored since an OpenSSL signature has been configured (setting "%s").',
2056
                    self::TIMESTAMP,
2057
                    self::ALGORITHM_KEY,
2058
                ),
2059
            );
2060
2061
            return null;
2062
        }
2063
2064
        return new DateTimeImmutable(
2065
            $timestamp,
2066
            new DateTimeZone('UTC'),
2067
        );
2068
    }
2069
2070
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2071
    {
2072
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2073
    }
2074
2075
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2076
    {
2077
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2078
    }
2079
2080
    /**
2081
     * @param bool $short Use the short version
2082
     *
2083
     * @return string the commit hash
2084
     */
2085
    private static function retrieveGitHash(string $path, bool $short = false): string
2086
    {
2087
        return self::runGitCommand(
2088
            sprintf(
2089
                'git log --pretty="%s" -n1 HEAD',
2090
                $short ? '%h' : '%H',
2091
            ),
2092
            $path,
2093
        );
2094
    }
2095
2096
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2097
    {
2098
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2099
    }
2100
2101
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2102
    {
2103
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2104
    }
2105
2106
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2107
    {
2108
        self::checkIfDefaultValue($logger, $raw, $key);
2109
2110
        return $raw->{$key} ?? null;
2111
    }
2112
2113
    private static function retrieveGitTag(string $path): string
2114
    {
2115
        return self::runGitCommand('git describe --tags HEAD', $path);
2116
    }
2117
2118
    private static function retrievePrettyGitTag(string $path): string
2119
    {
2120
        $version = self::retrieveGitTag($path);
2121
2122
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2123
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2124
        }
2125
2126
        return $version;
2127
    }
2128
2129
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2130
    {
2131
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2132
    }
2133
2134
    private static function retrieveGitVersion(string $path): ?string
2135
    {
2136
        try {
2137
            return self::retrieveGitTag($path);
2138
        } catch (RuntimeException $exception) {
2139
            try {
2140
                return self::retrieveGitHash($path, true);
2141
            } catch (RuntimeException $exception) {
2142
                throw new RuntimeException(
2143
                    sprintf(
2144
                        'The tag or commit hash could not be retrieved from "%s": %s',
2145
                        $path,
2146
                        $exception->getMessage(),
2147
                    ),
2148
                    0,
2149
                    $exception,
2150
                );
2151
            }
2152
        }
2153
    }
2154
2155
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2156
    {
2157
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2158
    }
2159
2160
    private static function retrieveDatetimeNow(string $format): string
2161
    {
2162
        return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format($format);
2163
    }
2164
2165
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2166
    {
2167
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2168
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2169
2170
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2171
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2172
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2173
            @trigger_error(
2174
                sprintf(
2175
                    'The "%s" is deprecated, use "%s" setting instead.',
2176
                    self::DATETIME_FORMAT_DEPRECATED_KEY,
2177
                    self::DATETIME_FORMAT_KEY,
2178
                ),
2179
                E_USER_DEPRECATED,
2180
            );
2181
            $logger->addWarning(
2182
                sprintf(
2183
                    'The "%s" is deprecated, use "%s" setting instead.',
2184
                    self::DATETIME_FORMAT_DEPRECATED_KEY,
2185
                    self::DATETIME_FORMAT_KEY,
2186
                ),
2187
            );
2188
2189
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2190
        } else {
2191
            $format = null;
2192
        }
2193
2194
        if (null !== $format) {
2195
            $formattedDate = (new DateTimeImmutable())->format($format);
2196
2197
            Assert::false(
2198
                false === $formattedDate || $formattedDate === $format,
2199
                sprintf(
2200
                    'Expected the datetime format to be a valid format: "%s" is not',
2201
                    $format,
2202
                ),
2203
            );
2204
2205
            return [$format, true];
2206
        }
2207
2208
        return [self::DEFAULT_DATETIME_FORMAT, false];
2209
    }
2210
2211
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2212
    {
2213
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2214
    }
2215
2216
    /**
2217
     * @return null|non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment null|non-empty-string at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in null|non-empty-string.
Loading history...
2218
     */
2219
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2220
    {
2221
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2222
2223
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2224
            return self::DEFAULT_SHEBANG;
2225
        }
2226
2227
        $shebang = $raw->{self::SHEBANG_KEY};
2228
2229
        if (false === $shebang) {
2230
            if (false === $stubIsGenerated) {
2231
                $logger->addRecommendation(
2232
                    sprintf(
2233
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2234
                        .' being used',
2235
                        self::SHEBANG_KEY,
2236
                    ),
2237
                );
2238
            }
2239
2240
            return null;
2241
        }
2242
2243
        Assert::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2244
2245
        $shebang = trim($shebang);
2246
2247
        Assert::notEmpty($shebang, 'The shebang should not be empty.');
2248
        Assert::true(
2249
            str_starts_with($shebang, '#!'),
2250
            sprintf(
2251
                'The shebang line must start with "#!". Got "%s" instead',
2252
                $shebang,
2253
            ),
2254
        );
2255
2256
        if (false === $stubIsGenerated) {
2257
            $logger->addWarning(
2258
                sprintf(
2259
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2260
                    .' used',
2261
                    self::SHEBANG_KEY,
2262
                ),
2263
            );
2264
        }
2265
2266
        return $shebang;
2267
    }
2268
2269
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): SigningAlgorithm
2270
    {
2271
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2272
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2273
        }
2274
2275
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2276
            return self::DEFAULT_SIGNING_ALGORITHM;
2277
        }
2278
2279
        $algorithmLabel = mb_strtoupper($raw->{self::ALGORITHM_KEY});
2280
        $algorithm = SigningAlgorithm::fromLabel($algorithmLabel);
2281
2282
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2283
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2284
        }
2285
2286
        if (SigningAlgorithm::OPENSSL === $algorithm) {
2287
            $logger->addWarning(
2288
                'Using an OpenSSL signature is deprecated and will be removed in 5.0.0. Please check '
2289
                .'https://github.com/box-project/box/blob/main/doc/phar-signing.md for alternatives.',
2290
            );
2291
        }
2292
2293
        return $algorithm;
2294
    }
2295
2296
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2297
    {
2298
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner());
2299
2300
        if (false === isset($raw->{self::BANNER_KEY})) {
2301
            return self::getDefaultBanner();
2302
        }
2303
2304
        $banner = $raw->{self::BANNER_KEY};
2305
2306
        if (false === $banner) {
2307
            if (false === $stubIsGenerated) {
2308
                $logger->addRecommendation(
2309
                    sprintf(
2310
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2311
                        .'being used',
2312
                        self::BANNER_KEY,
2313
                    ),
2314
                );
2315
            }
2316
2317
            return null;
2318
        }
2319
2320
        Assert::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2321
2322
        if (is_array($banner)) {
2323
            $banner = implode("\n", $banner);
2324
        }
2325
2326
        if (false === $stubIsGenerated) {
2327
            $logger->addWarning(
2328
                sprintf(
2329
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2330
                    self::BANNER_KEY,
2331
                ),
2332
            );
2333
        }
2334
2335
        return $banner;
2336
    }
2337
2338
    private static function getDefaultBanner(): string
2339
    {
2340
        return sprintf(self::DEFAULT_BANNER, get_box_version());
2341
    }
2342
2343
    private static function retrieveStubBannerPath(
2344
        stdClass $raw,
2345
        string $basePath,
2346
        bool $stubIsGenerated,
2347
        ConfigurationLogger $logger,
2348
    ): ?string {
2349
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2350
2351
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2352
            return null;
2353
        }
2354
2355
        $bannerFile = Path::makeAbsolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2356
2357
        Assert::file($bannerFile);
2358
2359
        if (false === $stubIsGenerated) {
2360
            $logger->addWarning(
2361
                sprintf(
2362
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2363
                    self::BANNER_FILE_KEY,
2364
                ),
2365
            );
2366
        }
2367
2368
        return $bannerFile;
2369
    }
2370
2371
    private static function normalizeStubBannerContents(?string $contents): ?string
2372
    {
2373
        if (null === $contents) {
2374
            return null;
2375
        }
2376
2377
        $banner = explode("\n", $contents);
2378
        $banner = array_map('trim', $banner);
2379
2380
        return implode("\n", $banner);
2381
    }
2382
2383
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2384
    {
2385
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2386
2387
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2388
            $stubPath = Path::makeAbsolute($raw->{self::STUB_KEY}, $basePath);
2389
2390
            Assert::file($stubPath);
2391
2392
            return $stubPath;
2393
        }
2394
2395
        return null;
2396
    }
2397
2398
    private static function retrieveInterceptsFileFunctions(
2399
        stdClass $raw,
2400
        bool $stubIsGenerated,
2401
        ConfigurationLogger $logger,
2402
    ): bool {
2403
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2404
2405
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2406
            return false;
2407
        }
2408
2409
        $intercept = $raw->{self::INTERCEPT_KEY};
2410
2411
        if ($intercept && false === $stubIsGenerated) {
2412
            $logger->addWarning(
2413
                sprintf(
2414
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2415
                    self::INTERCEPT_KEY,
2416
                ),
2417
            );
2418
        }
2419
2420
        return $intercept;
2421
    }
2422
2423
    private static function retrievePromptForPrivateKey(
2424
        stdClass $raw,
2425
        SigningAlgorithm $signingAlgorithm,
2426
        ConfigurationLogger $logger,
2427
    ): bool {
2428
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2429
            if (SigningAlgorithm::OPENSSL !== $signingAlgorithm) {
2430
                $logger->addWarning(
2431
                    'A prompt for password for the private key has been requested but ignored since the signing '
2432
                    .'algorithm used is not "OPENSSL.',
2433
                );
2434
2435
                return false;
2436
            }
2437
2438
            return true;
2439
        }
2440
2441
        return false;
2442
    }
2443
2444
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2445
    {
2446
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2447
2448
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2449
    }
2450
2451
    private static function retrieveCheckRequirements(
2452
        stdClass $raw,
2453
        bool $hasComposerJson,
2454
        bool $hasComposerLock,
2455
        bool $pharStubUsed,
2456
        ConfigurationLogger $logger,
2457
    ): bool {
2458
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2459
2460
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2461
            return $hasComposerJson || $hasComposerLock;
2462
        }
2463
2464
        /** @var bool $checkRequirements */
2465
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2466
2467
        // TODO: in 5.0 we no longer care about the composer.json
2468
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2469
            $logger->addWarning(
2470
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2471
                .'be found.',
2472
            );
2473
2474
            return false;
2475
        }
2476
2477
        if ($checkRequirements && false === $hasComposerLock) {
2478
            // TODO: in 5.0:
2479
            //  - adjust the warning
2480
            //  - return false here to skip the requirement checker
2481
            $logger->addWarning(
2482
                'Enabling the requirement checker when there is no composer.lock is deprecated. In the future the '
2483
                .'requirement checker will be forcefully skipped in this scenario.',
2484
            );
2485
        }
2486
2487
        if ($checkRequirements && $pharStubUsed) {
2488
            $logger->addWarning(
2489
                sprintf(
2490
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2491
                    .'used.',
2492
                    self::CHECK_REQUIREMENTS_KEY,
2493
                ),
2494
            );
2495
        }
2496
2497
        return $checkRequirements;
2498
    }
2499
2500
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2501
    {
2502
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2503
2504
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2505
            $configFilePath = Path::makeAbsolute(self::PHP_SCOPER_CONFIG, $basePath);
2506
            $configFilePath = file_exists($configFilePath) ? $configFilePath : null;
2507
2508
            return PhpScoperConfigurationFactory::create($configFilePath);
2509
        }
2510
2511
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2512
2513
        Assert::string($configFile);
2514
2515
        $configFilePath = Path::makeAbsolute($configFile, $basePath);
2516
2517
        Assert::file($configFilePath);
2518
        Assert::readable($configFilePath);
2519
2520
        return PhpScoperConfigurationFactory::create($configFilePath);
2521
    }
2522
2523
    /**
2524
     * Runs a Git command on the repository.
2525
     *
2526
     * @return string The trimmed output from the command
2527
     */
2528
    private static function runGitCommand(string $command, string $path): string
2529
    {
2530
        $process = Process::fromShellCommandline($command, $path);
2531
        $process->run();
2532
2533
        if ($process->isSuccessful()) {
2534
            return trim($process->getOutput());
2535
        }
2536
2537
        throw new RuntimeException(
2538
            sprintf(
2539
                'The tag or commit hash could not be retrieved from "%s": %s',
2540
                $path,
2541
                $process->getErrorOutput(),
2542
            ),
2543
            0,
2544
            new ProcessFailedException($process),
2545
        );
2546
    }
2547
2548
    /**
2549
     * @param string[] $compactorClasses
2550
     *
2551
     * @return string[]|null
2552
     */
2553
    private static function retrievePhpCompactorIgnoredAnnotations(
2554
        stdClass $raw,
2555
        array $compactorClasses,
2556
        ConfigurationLogger $logger,
2557
    ): ?array {
2558
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true);
2559
2560
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2561
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2562
2563
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2564
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2565
        }
2566
2567
        if (false === $hasPhpCompactor) {
2568
            $logger->addWarning(
2569
                sprintf(
2570
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2571
                    self::ANNOTATIONS_KEY,
2572
                ),
2573
            );
2574
        }
2575
2576
        /** @var null|bool|stdClass $annotations */
2577
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2578
2579
        if (true === $annotations || null === $annotations) {
2580
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2581
        }
2582
2583
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2584
            return null;
2585
        }
2586
2587
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2588
            $logger->addWarning(
2589
                sprintf(
2590
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2591
                    .' if it is set to `false`',
2592
                    self::ANNOTATIONS_KEY,
2593
                    self::IGNORED_ANNOTATIONS_KEY,
2594
                    self::ANNOTATIONS_KEY,
2595
                ),
2596
            );
2597
2598
            return null;
2599
        }
2600
2601
        $ignored = [];
2602
2603
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2604
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2605
        ) {
2606
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2607
2608
            return (array) $ignored;
2609
        }
2610
2611
        return $ignored;
2612
    }
2613
2614
    private static function createPhpCompactor(?array $ignoredAnnotations): Compactor
2615
    {
2616
        if (null === $ignoredAnnotations) {
0 ignored issues
show
introduced by
The condition null === $ignoredAnnotations is always false.
Loading history...
2617
            return new PhpCompactor(null);
2618
        }
2619
2620
        $ignoredAnnotations = array_values(
2621
            array_filter(
2622
                array_map(
2623
                    static fn (string $annotation): ?string => mb_strtolower(trim($annotation)),
2624
                    $ignoredAnnotations,
2625
                ),
2626
            ),
2627
        );
2628
2629
        return PhpCompactor::create($ignoredAnnotations);
2630
    }
2631
2632
    private static function createPhpScoperCompactor(
2633
        stdClass $raw,
2634
        string $basePath,
2635
        ConfigurationLogger $logger,
2636
    ): Compactor {
2637
        $phpScoperConfig = self::configurePhpScoperPrefix(
2638
            self::retrievePhpScoperConfig($raw, $basePath, $logger),
2639
        );
2640
2641
        $excludedFilePaths = array_values(
2642
            array_unique(
2643
                array_map(
2644
                    static fn (string $path): string => Path::makeRelative($path, $basePath),
2645
                    array_keys(
2646
                        $phpScoperConfig->getExcludedFilesWithContents(),
2647
                    ),
2648
                ),
2649
            ),
2650
        );
2651
2652
        return new PhpScoperCompactor(
2653
            new SerializableScoper($phpScoperConfig, ...$excludedFilePaths),
2654
        );
2655
    }
2656
2657
    private static function configurePhpScoperPrefix(PhpScoperConfiguration $phpScoperConfig): PhpScoperConfiguration
2658
    {
2659
        $prefix = $phpScoperConfig->getPrefix();
2660
        if (!str_starts_with($prefix, '_PhpScoper')) {
2661
            return $phpScoperConfig;
2662
        }
2663
2664
        return $phpScoperConfig->withPrefix(unique_id('_HumbugBox'));
2665
    }
2666
2667
    private static function checkIfDefaultValue(
2668
        ConfigurationLogger $logger,
2669
        stdClass $raw,
2670
        string $key,
2671
        $defaultValue = null,
2672
    ): void {
2673
        if (false === property_exists($raw, $key)) {
2674
            return;
2675
        }
2676
2677
        $value = $raw->{$key};
2678
2679
        if (null === $value
2680
            || (false === is_object($defaultValue) && $defaultValue === $value)
2681
            || (is_object($defaultValue) && $defaultValue == $value)
2682
        ) {
2683
            $logger->addRecommendation(
2684
                sprintf(
2685
                    'The "%s" setting can be omitted since is set to its default value',
2686
                    $key,
2687
                ),
2688
            );
2689
        }
2690
    }
2691
2692
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2693
    {
2694
        $logger->addRecommendation(
2695
            sprintf(
2696
                'The "%s" setting can be omitted since is set to its default value',
2697
                $key,
2698
            ),
2699
        );
2700
    }
2701
}
2702