Passed
Push — master ( c1a51a...efe97a )
by Théo
02:49
created

Configuration   F

Complexity

Total Complexity 314

Size/Duplication

Total Lines 2790
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 314
eloc 1317
dl 0
loc 2790
rs 0.8
c 0
b 0
f 0

109 Methods

Rating   Name   Duplication   Size   Complexity  
A getComposerJson() 0 3 1
A hasAutodiscoveredFiles() 0 3 1
A getBasePath() 0 3 1
A getConfigurationFile() 0 3 1
A getComposerLock() 0 3 1
A getBinaryFiles() 0 3 1
A dumpAutoload() 0 3 1
A excludeComposerFiles() 0 3 1
A getAlias() 0 3 1
A getDecodedComposerLockContents() 0 3 2
A export() 0 71 3
A getDecodedComposerJsonContents() 0 3 2
A getFiles() 0 3 1
A excludeDevFiles() 0 3 1
A __construct() 0 85 2
A retrieveMetadata() 0 11 3
A getStubPath() 0 3 1
A normalizePath() 0 3 1
A wrapInSplFileInfo() 0 7 1
A retrieveDatetimeFormat() 0 44 5
B retrieveStubBannerContents() 0 40 7
A retrieveDirectories() 0 26 3
A retrieveMap() 0 27 5
A retrievePrivateKeyPassphrase() 0 36 6
A retrieveGitHash() 0 8 2
A retrieveExcludeComposerFiles() 0 5 1
A retrieveShebang() 0 48 5
A getFileMode() 0 3 1
A retrieveCompressionAlgorithm() 0 27 3
A promptForPrivateKey() 0 3 1
B create() 0 165 6
A collectBinaryFiles() 0 30 1
A retrieveForceFilesAutodiscovery() 0 5 1
A collectFiles() 0 64 5
A getTmpOutputPath() 0 3 1
A retrieveDatetimeNow() 0 3 1
A retrieveGitHashPlaceholder() 0 3 1
A getMainScriptPath() 0 8 1
A getShebang() 0 3 1
A retrieveStubPath() 0 13 3
A autodiscoverFiles() 0 9 2
A retrieveGitTagPlaceholder() 0 3 1
C processFinder() 0 116 12
C retrieveDumpAutoload() 0 48 13
A retrieveFilesFromFinders() 0 17 2
A retrievePhpScoperConfig() 0 23 3
A retrieveGitVersionPlaceholder() 0 3 1
A getStubBannerPath() 0 3 1
A isStubGenerated() 0 3 1
A retrieveBasePath() 0 25 4
A checkRequirements() 0 3 1
A getCompactors() 0 3 1
A getStubBannerContents() 0 3 1
A processFinders() 0 11 1
A runGitCommand() 0 15 2
A getFileMapper() 0 3 1
A retrievePrettyGitTag() 0 9 2
A getDefaultBanner() 0 3 1
A getMetadata() 0 3 1
A retrieveGitShortHashPlaceholder() 0 3 1
A isInterceptFileFuncs() 0 3 1
A retrieveReplacementSigil() 0 3 1
A getSigningAlgorithm() 0 3 1
A retrieveMainScriptContents() 0 13 2
A retrieveBlacklist() 0 24 2
A retrieveCompactors() 0 27 2
A getPrivateKeyPath() 0 3 1
A retrieveSigningAlgorithm() 0 29 5
A isPrivateKeyPrompt() 0 3 1
A retrieveBlacklistFilter() 0 27 4
A retrievePrivateKeyPath() 0 34 5
B retrievePhpCompactorIgnoredAnnotations() 0 62 10
A getOutputPath() 0 3 1
A retrieveDirectoryPaths() 0 37 2
F retrieveAllDirectoriesToInclude() 0 156 23
D retrieveReplacements() 0 56 11
A retrieveGitTag() 0 3 1
B retrieveCheckRequirements() 0 36 8
A retrievePlaceholder() 0 5 1
A getPrivateKeyPassphrase() 0 3 1
A retrieveExcludeDevFiles() 0 20 4
A retrieveStubBannerPath() 0 26 3
A createPhpCompactor() 0 18 1
B retrieveOutputPath() 0 34 7
A retrieveFileMode() 0 19 5
A retrievePrettyGitPlaceholder() 0 3 1
A retrieveInterceptsFileFunctions() 0 23 4
A retrieveComposerFiles() 0 30 5
A getReplacements() 0 3 1
A getWarnings() 0 3 1
A retrieveGitVersion() 0 16 3
A retrievePromptForPrivateKey() 0 19 4
A retrieveDatetimeNowPlaceHolder() 0 3 1
A getRecommendations() 0 3 1
A checkCompactorsOrder() 0 20 5
B retrieveAllFiles() 0 106 4
A retrieveIsStubGenerated() 0 5 3
A retrieveFilesAggregate() 0 11 3
A normalizeStubBannerContents() 0 10 2
A retrieveFiles() 0 51 4
A hasMainScript() 0 3 1
A getCompressionAlgorithm() 0 3 1
A retrieveAlias() 0 22 3
B retrieveMainScriptPath() 0 48 9
B createCompactors() 0 43 6
A getMainScriptContents() 0 8 1
A createPhpScoperCompactor() 0 38 2
B checkIfDefaultValue() 0 20 7
A addRecommendationForDefaultValue() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Configuration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Configuration, and based on these observations, apply Extract Interface, too.

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