Passed
Push — master ( bf567e...1b8359 )
by Théo
01:59
created

Configuration::retrieveDumpAutoload()   C

Complexity

Conditions 13
Paths 136

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 136
nop 3
dl 0
loc 48
rs 6.3166

How to fix   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 Humbug\PhpScoper\ConfigurationFactory;
18
use Symfony\Component\Filesystem\Filesystem;
19
use function array_diff;
20
use function array_filter;
21
use function array_flip;
22
use function array_key_exists;
23
use function array_keys;
24
use function array_map;
25
use function array_merge;
26
use function array_unique;
27
use function array_values;
28
use function array_walk;
29
use Closure;
30
use function constant;
31
use function current;
32
use DateTimeImmutable;
33
use DateTimeZone;
34
use function defined;
35
use function dirname;
36
use const E_USER_DEPRECATED;
37
use function explode;
38
use function file_exists;
39
use function getcwd;
40
use Herrera\Box\Compactor\Json as LegacyJson;
41
use Herrera\Box\Compactor\Php as LegacyPhp;
42
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
43
use Humbug\PhpScoper\Container;
44
use Humbug\PhpScoper\Scoper;
45
use Humbug\PhpScoper\Scoper\FileWhitelistScoper;
46
use function implode;
47
use function in_array;
48
use function intval;
49
use InvalidArgumentException;
50
use function is_array;
51
use function is_bool;
52
use function is_file;
53
use function is_link;
54
use function is_object;
55
use function is_readable;
56
use function is_string;
57
use function iter\map;
58
use function iter\toArray;
59
use function iter\values;
60
use KevinGH\Box\Annotation\CompactedFormatter;
61
use KevinGH\Box\Annotation\DocblockAnnotationParser;
62
use KevinGH\Box\Compactor\Compactor;
63
use KevinGH\Box\Compactor\Compactors;
64
use KevinGH\Box\Compactor\Json as JsonCompactor;
65
use KevinGH\Box\Compactor\Php as PhpCompactor;
66
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
67
use KevinGH\Box\Composer\ComposerConfiguration;
68
use KevinGH\Box\Composer\ComposerFile;
69
use KevinGH\Box\Composer\ComposerFiles;
70
use function KevinGH\Box\FileSystem\canonicalize;
71
use function KevinGH\Box\FileSystem\file_contents;
72
use function KevinGH\Box\FileSystem\is_absolute_path;
73
use function KevinGH\Box\FileSystem\longest_common_base_path;
74
use function KevinGH\Box\FileSystem\make_path_absolute;
75
use function KevinGH\Box\FileSystem\make_path_relative;
76
use function KevinGH\Box\get_box_version;
77
use function KevinGH\Box\get_phar_compression_algorithms;
78
use function KevinGH\Box\get_phar_signing_algorithms;
79
use KevinGH\Box\Json\Json;
80
use KevinGH\Box\MapFile;
81
use KevinGH\Box\PhpScoper\SerializablePhpScoper;
82
use KevinGH\Box\PhpScoper\SimpleScoper;
83
use function KevinGH\Box\unique_id;
84
use function krsort;
85
use Phar;
86
use phpDocumentor\Reflection\DocBlockFactory;
87
use function preg_match;
88
use function preg_replace;
89
use function property_exists;
90
use function realpath;
91
use RuntimeException;
92
use Seld\JsonLint\ParsingException;
93
use function sort;
94
use const SORT_STRING;
95
use SplFileInfo;
96
use function sprintf;
97
use stdClass;
98
use function strtoupper;
99
use function substr;
100
use Symfony\Component\Finder\Finder;
101
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
102
use Symfony\Component\Process\Process;
103
use Symfony\Component\VarDumper\Cloner\VarCloner;
104
use Symfony\Component\VarDumper\Dumper\CliDumper;
105
use function trigger_error;
106
use function trim;
107
use Webmozart\Assert\Assert;
108
109
/**
110
 * @private
111
 */
112
final class Configuration
113
{
114
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
115
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
116
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
117
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
118
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
119
    private const DEFAULT_BANNER = <<<'BANNER'
120
Generated by Humbug Box %s.
121
122
@link https://github.com/humbug/box
123
BANNER;
124
    private const FILES_SETTINGS = [
125
        'directories',
126
        'finder',
127
    ];
128
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
129
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
130
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
131
132
    private const DEFAULT_IGNORED_ANNOTATIONS = [
133
        'abstract',
134
        'access',
135
        'annotation',
136
        'api',
137
        'attribute',
138
        'attributes',
139
        'author',
140
        'category',
141
        'code',
142
        'codecoverageignore',
143
        'codecoverageignoreend',
144
        'codecoverageignorestart',
145
        'copyright',
146
        'deprec',
147
        'deprecated',
148
        'endcode',
149
        'example',
150
        'exception',
151
        'filesource',
152
        'final',
153
        'fixme',
154
        'global',
155
        'ignore',
156
        'ingroup',
157
        'inheritdoc',
158
        'internal',
159
        'license',
160
        'link',
161
        'magic',
162
        'method',
163
        'name',
164
        'override',
165
        'package',
166
        'package_version',
167
        'param',
168
        'private',
169
        'property',
170
        'required',
171
        'return',
172
        'see',
173
        'since',
174
        'static',
175
        'staticvar',
176
        'subpackage',
177
        'suppresswarnings',
178
        'target',
179
        'throw',
180
        'throws',
181
        'todo',
182
        'tutorial',
183
        'usedby',
184
        'uses',
185
        'var',
186
        'version',
187
    ];
188
189
    private const ALGORITHM_KEY = 'algorithm';
190
    private const ALIAS_KEY = 'alias';
191
    private const ANNOTATIONS_KEY = 'annotations';
192
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
193
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
194
    private const BANNER_KEY = 'banner';
195
    private const BANNER_FILE_KEY = 'banner-file';
196
    private const BASE_PATH_KEY = 'base-path';
197
    private const BLACKLIST_KEY = 'blacklist';
198
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
199
    private const CHMOD_KEY = 'chmod';
200
    private const COMPACTORS_KEY = 'compactors';
201
    private const COMPRESSION_KEY = 'compression';
202
    private const DATETIME_KEY = 'datetime';
203
    private const DATETIME_FORMAT_KEY = 'datetime-format';
204
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
205
    private const DIRECTORIES_KEY = 'directories';
206
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
207
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
208
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
209
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
210
    private const FILES_KEY = 'files';
211
    private const FILES_BIN_KEY = 'files-bin';
212
    private const FINDER_KEY = 'finder';
213
    private const FINDER_BIN_KEY = 'finder-bin';
214
    private const GIT_KEY = 'git';
215
    private const GIT_COMMIT_KEY = 'git-commit';
216
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
217
    private const GIT_TAG_KEY = 'git-tag';
218
    private const GIT_VERSION_KEY = 'git-version';
219
    private const INTERCEPT_KEY = 'intercept';
220
    private const KEY_KEY = 'key';
221
    private const KEY_PASS_KEY = 'key-pass';
222
    private const MAIN_KEY = 'main';
223
    private const MAP_KEY = 'map';
224
    private const METADATA_KEY = 'metadata';
225
    private const OUTPUT_KEY = 'output';
226
    private const PHP_SCOPER_KEY = 'php-scoper';
227
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
228
    private const REPLACEMENTS_KEY = 'replacements';
229
    private const SHEBANG_KEY = 'shebang';
230
    private const STUB_KEY = 'stub';
231
232
    private $file;
233
    private $fileMode;
234
    private $alias;
235
    private $basePath;
236
    private $composerJson;
237
    private $composerLock;
238
    private $files;
239
    private $binaryFiles;
240
    private $autodiscoveredFiles;
241
    private $dumpAutoload;
242
    private $excludeComposerFiles;
243
    private $excludeDevFiles;
244
    private $compactors;
245
    private $compressionAlgorithm;
246
    private $mainScriptPath;
247
    private $mainScriptContents;
248
    private $fileMapper;
249
    private $metadata;
250
    private $tmpOutputPath;
251
    private $outputPath;
252
    private $privateKeyPassphrase;
253
    private $privateKeyPath;
254
    private $promptForPrivateKey;
255
    private $processedReplacements;
256
    private $shebang;
257
    private $signingAlgorithm;
258
    private $stubBannerContents;
259
    private $stubBannerPath;
260
    private $stubPath;
261
    private $isInterceptFileFuncs;
262
    private $isStubGenerated;
263
    private $checkRequirements;
264
    private $warnings;
265
    private $recommendations;
266
267
    public static function create(?string $file, stdClass $raw): self
268
    {
269
        $logger = new ConfigurationLogger();
270
271
        $basePath = self::retrieveBasePath($file, $raw, $logger);
272
273
        $composerFiles = self::retrieveComposerFiles($basePath);
274
275
        $dumpAutoload = self::retrieveDumpAutoload($raw, $composerFiles, $logger);
276
277
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
278
279
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles->getComposerJson()->getDecodedContents(), $logger);
280
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
281
282
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
283
284
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
285
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
286
287
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
288
289
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
290
291
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
292
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
293
294
        if (null !== $stubBannerPath) {
295
            $stubBannerContents = file_contents($stubBannerPath);
296
        }
297
298
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
299
300
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
301
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
302
        }
303
304
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
305
306
        $checkRequirements = self::retrieveCheckRequirements(
307
            $raw,
308
            null !== $composerFiles->getComposerJson()->getPath(),
309
            null !== $composerFiles->getComposerLock()->getPath(),
310
            false === $isStubGenerated && null === $stubPath,
311
            $logger
312
        );
313
314
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
315
316
        $devPackages = ComposerConfiguration::retrieveDevPackages(
317
            $basePath,
318
            $composerFiles->getComposerJson()->getDecodedContents(),
319
            $composerFiles->getComposerLock()->getDecodedContents(),
320
            $excludeDevPackages
321
        );
322
323
        /**
324
         * @var string[] $excludedPaths
325
         * @var Closure  $blacklistFilter
326
         */
327
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
328
            $raw,
329
            $basePath,
330
            $logger,
331
            $tmpOutputPath,
332
            $outputPath,
333
            $mainScriptPath
334
        );
335
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
336
        // If you consider the following:
337
        //
338
        // {
339
        //   "files": ["file1"],
340
        //   "blacklist": ["file1"],
341
        // }
342
        //
343
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
344
        // and directories but the user should always have the possibility to force his way to include a file.
345
        //
346
        // The exception however, is for the following which is essential for the good functioning of Box
347
        $alwaysExcludedPaths = array_map(
348
            static function (string $excludedPath) use ($basePath): string {
349
                return self::normalizePath($excludedPath, $basePath);
350
            },
351
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath])
352
        );
353
354
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
355
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
356
357
        $filesAggregate = self::collectFiles(
358
            $raw,
359
            $basePath,
360
            $mainScriptPath,
361
            $blacklistFilter,
362
            $excludedPaths,
363
            $alwaysExcludedPaths,
364
            $devPackages,
365
            $composerFiles,
366
            $autodiscoverFiles,
367
            $forceFilesAutodiscovery,
368
            $logger
369
        );
370
        $binaryFilesAggregate = self::collectBinaryFiles(
371
            $raw,
372
            $basePath,
373
            $blacklistFilter,
374
            $excludedPaths,
375
            $alwaysExcludedPaths,
376
            $devPackages,
377
            $logger
378
        );
379
380
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
381
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
382
383
        $fileMode = self::retrieveFileMode($raw, $logger);
384
385
        $map = self::retrieveMap($raw, $logger);
386
        $fileMapper = new MapFile($basePath, $map);
387
388
        $metadata = self::retrieveMetadata($raw, $logger);
389
390
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
391
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
392
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
393
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
394
395
        $replacements = self::retrieveReplacements($raw, $file, $basePath, $logger);
396
397
        return new self(
398
            $file,
399
            $alias,
400
            $basePath,
401
            $composerFiles->getComposerJson(),
402
            $composerFiles->getComposerLock(),
403
            $filesAggregate,
404
            $binaryFilesAggregate,
405
            $autodiscoverFiles || $forceFilesAutodiscovery,
406
            $dumpAutoload,
407
            $excludeComposerFiles,
408
            $excludeDevPackages,
409
            $compactors,
410
            $compressionAlgorithm,
411
            $fileMode,
412
            $mainScriptPath,
413
            $mainScriptContents,
414
            $fileMapper,
415
            $metadata,
416
            $tmpOutputPath,
417
            $outputPath,
418
            $privateKeyPassphrase,
419
            $privateKeyPath,
420
            $promptForPrivateKey,
421
            $replacements,
422
            $shebang,
423
            $signingAlgorithm,
424
            $stubBannerContents,
425
            $stubBannerPath,
426
            $stubPath,
427
            $isInterceptsFileFunctions,
428
            $isStubGenerated,
429
            $checkRequirements,
430
            $logger->getWarnings(),
431
            $logger->getRecommendations()
432
        );
433
    }
434
435
    /**
436
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
437
     *                                            path relative to it (the base path)
438
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
439
     *                                            string and the second element its decoded contents as an
440
     *                                            associative array.
441
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
442
     *                                            string and the second element its decoded contents as an
443
     *                                            associative array.
444
     * @param SplFileInfo[] $files                List of files
445
     * @param SplFileInfo[] $binaryFiles          List of binary files
446
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
447
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
448
     *                                            installed.json should be removed from the PHAR
449
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
450
     * @param null|int      $fileMode             File mode in octal form
451
     * @param string        $mainScriptPath       The main script file path
452
     * @param string        $mainScriptContents   The processed content of the main script file
453
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
454
     * @param mixed         $metadata             The PHAR Metadata
455
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
456
     * @param array         $replacements         The processed list of replacement placeholders and their values
457
     * @param null|string   $shebang              The shebang line
458
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
459
     * @param null|string   $stubBannerContents   The stub banner comment
460
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
461
     * @param null|string   $stubPath             The PHAR stub file path
462
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
463
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
464
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
465
     *                                            running
466
     * @param string[]      $warnings
467
     * @param string[]      $recommendations
468
     */
469
    private function __construct(
470
        ?string $file,
471
        string $alias,
472
        string $basePath,
473
        ComposerFile $composerJson,
474
        ComposerFile $composerLock,
475
        array $files,
476
        array $binaryFiles,
477
        bool $autodiscoveredFiles,
478
        bool $dumpAutoload,
479
        bool $excludeComposerFiles,
480
        bool $excludeDevPackages,
481
        Compactors $compactors,
482
        ?int $compressionAlgorithm,
483
        ?int $fileMode,
484
        ?string $mainScriptPath,
485
        ?string $mainScriptContents,
486
        MapFile $fileMapper,
487
        $metadata,
488
        string $tmpOutputPath,
489
        string $outputPath,
490
        ?string $privateKeyPassphrase,
491
        ?string $privateKeyPath,
492
        bool $promptForPrivateKey,
493
        array $replacements,
494
        ?string $shebang,
495
        int $signingAlgorithm,
496
        ?string $stubBannerContents,
497
        ?string $stubBannerPath,
498
        ?string $stubPath,
499
        bool $isInterceptFileFuncs,
500
        bool $isStubGenerated,
501
        bool $checkRequirements,
502
        array $warnings,
503
        array $recommendations
504
    ) {
505
        Assert::nullOrInArray(
506
            $compressionAlgorithm,
507
            get_phar_compression_algorithms(),
508
            sprintf(
509
                'Invalid compression algorithm %%s, use one of "%s" instead.',
510
                implode('", "', array_keys(get_phar_compression_algorithms()))
511
            )
512
        );
513
514
        if (null === $mainScriptPath) {
515
            Assert::null($mainScriptContents);
516
        } else {
517
            Assert::notNull($mainScriptContents);
518
        }
519
520
        $this->file = $file;
521
        $this->alias = $alias;
522
        $this->basePath = $basePath;
523
        $this->composerJson = $composerJson;
524
        $this->composerLock = $composerLock;
525
        $this->files = $files;
526
        $this->binaryFiles = $binaryFiles;
527
        $this->autodiscoveredFiles = $autodiscoveredFiles;
528
        $this->dumpAutoload = $dumpAutoload;
529
        $this->excludeComposerFiles = $excludeComposerFiles;
530
        $this->excludeDevFiles = $excludeDevPackages;
531
        $this->compactors = $compactors;
532
        $this->compressionAlgorithm = $compressionAlgorithm;
533
        $this->fileMode = $fileMode;
534
        $this->mainScriptPath = $mainScriptPath;
535
        $this->mainScriptContents = $mainScriptContents;
536
        $this->fileMapper = $fileMapper;
537
        $this->metadata = $metadata;
538
        $this->tmpOutputPath = $tmpOutputPath;
539
        $this->outputPath = $outputPath;
540
        $this->privateKeyPassphrase = $privateKeyPassphrase;
541
        $this->privateKeyPath = $privateKeyPath;
542
        $this->promptForPrivateKey = $promptForPrivateKey;
543
        $this->processedReplacements = $replacements;
544
        $this->shebang = $shebang;
545
        $this->signingAlgorithm = $signingAlgorithm;
546
        $this->stubBannerContents = $stubBannerContents;
547
        $this->stubBannerPath = $stubBannerPath;
548
        $this->stubPath = $stubPath;
549
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
550
        $this->isStubGenerated = $isStubGenerated;
551
        $this->checkRequirements = $checkRequirements;
552
        $this->warnings = $warnings;
553
        $this->recommendations = $recommendations;
554
    }
555
556
    public function export(): string
557
    {
558
        $exportedConfig = clone $this;
559
560
        $basePath = $exportedConfig->basePath;
561
562
        /**
563
         * @param null|SplFileInfo|string $path
564
         */
565
        $normalizePath = static function ($path) use ($basePath): ?string {
566
            if (null === $path) {
567
                return null;
568
            }
569
570
            if ($path instanceof SplFileInfo) {
571
                $path = $path->getPathname();
572
            }
573
574
            return make_path_relative($path, $basePath);
575
        };
576
577
        $normalizeProperty = static function (&$property) use ($normalizePath): void {
578
            $property = $normalizePath($property);
579
        };
580
581
        $normalizeFiles = static function (&$files) use ($normalizePath): void {
582
            $files = array_map($normalizePath, $files);
583
            sort($files, SORT_STRING);
584
        };
585
586
        $normalizeFiles($exportedConfig->files);
587
        $normalizeFiles($exportedConfig->binaryFiles);
588
589
        $exportedConfig->composerJson = new ComposerFile(
590
            $normalizePath($exportedConfig->composerJson->getPath()),
591
            $exportedConfig->composerJson->getDecodedContents()
592
        );
593
        $exportedConfig->composerLock = new ComposerFile(
594
            $normalizePath($exportedConfig->composerLock->getPath()),
595
            $exportedConfig->composerLock->getDecodedContents()
596
        );
597
598
        $normalizeProperty($exportedConfig->file);
599
        $normalizeProperty($exportedConfig->mainScriptPath);
600
        $normalizeProperty($exportedConfig->tmpOutputPath);
601
        $normalizeProperty($exportedConfig->outputPath);
602
        $normalizeProperty($exportedConfig->privateKeyPath);
603
        $normalizeProperty($exportedConfig->stubBannerPath);
604
        $normalizeProperty($exportedConfig->stubPath);
605
606
        $exportedConfig->compressionAlgorithm = array_flip(get_phar_compression_algorithms())[$exportedConfig->compressionAlgorithm ?? Phar::NONE];
607
        $exportedConfig->signingAlgorithm = array_flip(get_phar_signing_algorithms())[$exportedConfig->signingAlgorithm];
608
        $exportedConfig->compactors = array_map('get_class', $exportedConfig->compactors->toArray());
609
        $exportedConfig->fileMode = '0'.decoct($exportedConfig->fileMode);
0 ignored issues
show
Bug introduced by
It seems like $exportedConfig->fileMode can also be of type null; however, parameter $num of decoct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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