Completed
Pull Request — master (#366)
by Théo
15:28 queued 07:20
created

Configuration   B

Complexity

Total Complexity 11

Size/Duplication

Total Lines 2742
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 1289
dl 0
loc 2742
rs 8.8
c 0
b 0
f 0

111 Methods

Rating   Name   Duplication   Size   Complexity  
A getWarnings() 0 3 1
A retrieveDatetimeFormat() 0 34 5
A retrieveInterceptsFileFunctions() 0 23 4
A getFileMapper() 0 3 1
A getStubBannerContents() 0 3 1
A retrieveMainScriptContents() 0 13 2
A retrievePlaceholder() 0 5 1
A retrieveMetadata() 0 11 3
A retrieveGitTag() 0 3 1
A retrieveGitHashPlaceholder() 0 3 1
A retrieveDatetimeNow() 0 5 1
B retrieveCompactors() 0 72 11
A getConfigurationFile() 0 3 1
B retrieveMainScriptPath() 0 43 9
B retrieveFiles() 0 56 6
A getTmpOutputPath() 0 3 1
A getShebang() 0 3 1
A checkRequirements() 0 3 1
A wrapInSplFileInfo() 0 7 1
A getBinaryFiles() 0 3 1
A createPhpCompactor() 0 18 1
B retrieveStubBannerContents() 0 40 7
A hp$0 ➔ createScoper() 0 3 1
A retrieveForceFilesAutodiscovery() 0 5 1
A getComposerJson() 0 3 1
A retrievePromptForPrivateKey() 0 19 4
A normalizePath() 0 3 1
A promptForPrivateKey() 0 3 1
A retrieveDumpAutoload() 0 21 5
B retrieveOutputPath() 0 34 7
A excludeComposerFiles() 0 3 1
A retrieveFilesFromFinders() 0 17 2
A getStubBannerPath() 0 3 1
B collectFiles() 0 88 9
A dumpAutoload() 0 3 1
A retrieveBlacklistFilter() 0 27 4
A isInterceptFileFuncs() 0 3 1
A retrieveGitTagPlaceholder() 0 3 1
A getFileMode() 0 3 1
A getRecommendations() 0 3 1
addRecommendationForDefaultValue() 0 6 ?
A retrieveAlias() 0 22 3
A retrieveReplacementSigil() 0 3 1
A getMainScriptPath() 0 8 1
A retrievePrivateKeyPassphrase() 0 36 6
A retrieveGitHash() 0 8 2
A retrieveBasePath() 0 25 4
A retrieveMap() 0 28 5
A getAlias() 0 3 1
A getReplacements() 0 3 1
A collectBinaryFiles() 0 30 1
B retrieveAllFiles() 0 106 4
A getStubPath() 0 3 1
A retrieveStubBannerPath() 0 26 3
B retrievePhpCompactorIgnoredAnnotations() 0 59 10
A excludeDevFiles() 0 3 1
A getBasePath() 0 3 1
D retrieveReplacements() 0 56 11
A retrievePhpScoperConfig() 0 23 3
B retrieveCheckRequirements() 0 36 8
A retrieveIsStubGenerated() 0 5 3
A retrievePrettyGitTag() 0 9 2
A retrieveBlacklist() 0 24 2
A retrieveSigningAlgorithm() 0 29 5
A retrieveExcludeComposerFiles() 0 5 1
A retrieveShebang() 0 48 5
A getComposerLock() 0 3 1
A isStubGenerated() 0 3 1
A hp$0 ➔ addRecommendationForDefaultValue() 0 6 1
A retrieveFilesAggregate() 0 11 3
A getSigningAlgorithm() 0 3 1
A runGitCommand() 0 15 2
checkIfDefaultValue() 0 20 ?
A getCompressionAlgorithm() 0 3 1
A retrieveExcludeDevFiles() 0 20 4
A getPrivateKeyPassphrase() 0 3 1
createPhpScoperCompactor() 0 34 ?
A normalizeStubBannerContents() 0 10 2
F retrieveAllDirectoriesToInclude() 0 150 23
A retrievePrettyGitPlaceholder() 0 3 1
A getFiles() 0 3 1
A getDefaultBanner() 0 3 1
A retrieveGitVersionPlaceholder() 0 3 1
A hasMainScript() 0 3 1
C processFinder() 0 112 11
A retrievePrivateKeyPath() 0 34 5
A getMetadata() 0 3 1
A retrieveDirectories() 0 26 3
A getOutputPath() 0 3 1
A processFinders() 0 11 1
A retrieveGitVersion() 0 16 3
A retrieveDirectoryPaths() 0 37 2
A retrieveCompressionAlgorithm() 0 27 3
A hp$0 ➔ createPhpScoperCompactor() 0 34 2
A retrieveFileMode() 0 19 5
A __construct() 0 85 2
A autodiscoverFiles() 0 9 2
A getPrivateKeyPath() 0 3 1
A isPrivateKeyPrompt() 0 3 1
A getDecodedComposerJsonContents() 0 3 1
A export() 0 55 3
A retrieveComposerFiles() 0 30 5
A hasAutodiscoveredFiles() 0 3 1
A retrieveStubPath() 0 13 3
A retrieveGitShortHashPlaceholder() 0 3 1
A retrieveDatetimeNowPlaceHolder() 0 3 1
A getDecodedComposerLockContents() 0 3 1
B hp$0 ➔ checkIfDefaultValue() 0 20 7
C create() 0 181 9
A getMainScriptContents() 0 8 1
A getCompactors() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use function array_column;
18
use function array_diff;
19
use function array_filter;
20
use function array_flip;
21
use function array_key_exists;
22
use function array_keys;
23
use function array_map;
24
use function array_merge;
25
use function array_unique;
26
use function array_values;
27
use function array_walk;
28
use Assert\Assertion;
29
use Closure;
30
use function constant;
31
use function current;
32
use DateTimeImmutable;
33
use DateTimeZone;
34
use function defined;
35
use function dirname;
36
use const E_USER_DEPRECATED;
37
use function explode;
38
use function file_exists;
39
use function getcwd;
40
use Herrera\Box\Compactor\Json as LegacyJson;
41
use Herrera\Box\Compactor\Php as LegacyPhp;
42
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
43
use Humbug\PhpScoper\Console\ApplicationFactory;
44
use Humbug\PhpScoper\Scoper;
45
use Humbug\PhpScoper\Scoper\FileWhitelistScoper;
46
use function implode;
47
use function in_array;
48
use function intval;
49
use InvalidArgumentException;
50
use function is_array;
51
use function is_bool;
52
use function is_file;
53
use function is_link;
54
use function is_object;
55
use function is_readable;
56
use function is_string;
57
use function iter\map;
58
use function iter\toArray;
59
use function iter\values;
60
use KevinGH\Box\Annotation\AnnotationDumper;
61
use KevinGH\Box\Annotation\DocblockAnnotationParser;
62
use KevinGH\Box\Annotation\DocblockParser;
63
use KevinGH\Box\Compactor\Json as JsonCompactor;
64
use KevinGH\Box\Compactor\Php as PhpCompactor;
65
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
66
use KevinGH\Box\Composer\ComposerConfiguration;
67
use function KevinGH\Box\FileSystem\canonicalize;
68
use function KevinGH\Box\FileSystem\file_contents;
69
use function KevinGH\Box\FileSystem\is_absolute_path;
70
use function KevinGH\Box\FileSystem\longest_common_base_path;
71
use function KevinGH\Box\FileSystem\make_path_absolute;
72
use function KevinGH\Box\FileSystem\make_path_relative;
73
use KevinGH\Box\Json\Json;
74
use KevinGH\Box\PhpScoper\SimpleScoper;
75
use function krsort;
76
use Phar;
77
use function preg_match;
78
use function preg_replace;
79
use function property_exists;
80
use function realpath;
81
use RuntimeException;
82
use Seld\JsonLint\ParsingException;
83
use SplFileInfo;
84
use function sprintf;
85
use stdClass;
86
use function strtoupper;
87
use function substr;
88
use Symfony\Component\Finder\Finder;
89
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
90
use Symfony\Component\Process\Process;
91
use Symfony\Component\VarDumper\Cloner\VarCloner;
92
use Symfony\Component\VarDumper\Dumper\CliDumper;
93
use function trigger_error;
94
use function trim;
95
96
/**
97
 * @private
98
 */
99
final class Configuration
100
{
101
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
102
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
103
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
104
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
105
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
106
    private const DEFAULT_BANNER = <<<'BANNER'
107
Generated by Humbug Box %s.
108
109
@link https://github.com/humbug/box
110
BANNER;
111
    private const FILES_SETTINGS = [
112
        'directories',
113
        'finder',
114
    ];
115
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
116
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
117
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
118
119
    private const DEFAULT_IGNORED_ANNOTATIONS = [
120
        'abstract',
121
        'access',
122
        'annotation',
123
        'api',
124
        'attribute',
125
        'attributes',
126
        'author',
127
        'category',
128
        'code',
129
        'codecoverageignore',
130
        'codecoverageignoreend',
131
        'codecoverageignorestart',
132
        'copyright',
133
        'deprec',
134
        'deprecated',
135
        'endcode',
136
        'example',
137
        'exception',
138
        'filesource',
139
        'final',
140
        'fixme',
141
        'global',
142
        'ignore',
143
        'ingroup',
144
        'inheritdoc',
145
        'internal',
146
        'license',
147
        'link',
148
        'magic',
149
        'method',
150
        'name',
151
        'override',
152
        'package',
153
        'package_version',
154
        'param',
155
        'private',
156
        'property',
157
        'required',
158
        'return',
159
        'see',
160
        'since',
161
        'static',
162
        'staticvar',
163
        'subpackage',
164
        'suppresswarnings',
165
        'target',
166
        'throw',
167
        'throws',
168
        'todo',
169
        'tutorial',
170
        'usedby',
171
        'uses',
172
        'var',
173
        'version',
174
    ];
175
176
    private const ALGORITHM_KEY = 'algorithm';
177
    private const ALIAS_KEY = 'alias';
178
    private const ANNOTATIONS_KEY = 'annotations';
179
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
180
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
181
    private const BANNER_KEY = 'banner';
182
    private const BANNER_FILE_KEY = 'banner-file';
183
    private const BASE_PATH_KEY = 'base-path';
184
    private const BLACKLIST_KEY = 'blacklist';
185
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
186
    private const CHMOD_KEY = 'chmod';
187
    private const COMPACTORS_KEY = 'compactors';
188
    private const COMPRESSION_KEY = 'compression';
189
    private const DATETIME_KEY = 'datetime';
190
    private const DATETIME_FORMAT_KEY = 'datetime-format';
191
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
192
    private const DIRECTORIES_KEY = 'directories';
193
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
194
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
195
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
196
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
197
    private const FILES_KEY = 'files';
198
    private const FILES_BIN_KEY = 'files-bin';
199
    private const FINDER_KEY = 'finder';
200
    private const FINDER_BIN_KEY = 'finder-bin';
201
    private const GIT_KEY = 'git';
202
    private const GIT_COMMIT_KEY = 'git-commit';
203
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
204
    private const GIT_TAG_KEY = 'git-tag';
205
    private const GIT_VERSION_KEY = 'git-version';
206
    private const INTERCEPT_KEY = 'intercept';
207
    private const KEY_KEY = 'key';
208
    private const KEY_PASS_KEY = 'key-pass';
209
    private const MAIN_KEY = 'main';
210
    private const MAP_KEY = 'map';
211
    private const METADATA_KEY = 'metadata';
212
    private const OUTPUT_KEY = 'output';
213
    private const PHP_SCOPER_KEY = 'php-scoper';
214
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
215
    private const REPLACEMENTS_KEY = 'replacements';
216
    private const SHEBANG_KEY = 'shebang';
217
    private const STUB_KEY = 'stub';
218
219
    private $file;
220
    private $fileMode;
221
    private $alias;
222
    private $basePath;
223
    private $composerJson;
224
    private $composerLock;
225
    private $files;
226
    private $binaryFiles;
227
    private $autodiscoveredFiles;
228
    private $dumpAutoload;
229
    private $excludeComposerFiles;
230
    private $excludeDevFiles;
231
    private $compactors;
232
    private $compressionAlgorithm;
233
    private $mainScriptPath;
234
    private $mainScriptContents;
235
    private $fileMapper;
236
    private $metadata;
237
    private $tmpOutputPath;
238
    private $outputPath;
239
    private $privateKeyPassphrase;
240
    private $privateKeyPath;
241
    private $promptForPrivateKey;
242
    private $processedReplacements;
243
    private $shebang;
244
    private $signingAlgorithm;
245
    private $stubBannerContents;
246
    private $stubBannerPath;
247
    private $stubPath;
248
    private $isInterceptFileFuncs;
249
    private $isStubGenerated;
250
    private $checkRequirements;
251
    private $warnings;
252
    private $recommendations;
253
254
    public static function create(?string $file, stdClass $raw): self
255
    {
256
        $logger = new ConfigurationLogger();
257
258
        $basePath = self::retrieveBasePath($file, $raw, $logger);
259
260
        $composerFiles = self::retrieveComposerFiles($basePath);
261
262
        /**
263
         * @var (string|null)[]
264
         * @var (string|null)[] $composerLock
265
         * @var (string|null)[] $installedJson
266
         */
267
        [$composerJson, $composerLock, $installedJson] = $composerFiles;
268
269
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
270
271
        if ($dumpAutoload && null !== $installedJson[0] && null === $composerLock[0]) {
272
            $logger->addWarning(
273
                'A vendor/composer/installed.json file has been found but its related file composer.lock could not. '
274
                .'This is likely due to the file having been removed despite being necessary. This will not break the '
275
                .'build but the dump-autoload had to be disabled.'
276
            );
277
        }
278
279
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
280
281
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
282
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
283
284
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
285
286
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
287
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
288
289
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
290
291
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
292
293
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
294
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
295
296
        if (null !== $stubBannerPath) {
297
            $stubBannerContents = file_contents($stubBannerPath);
298
        }
299
300
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
301
302
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
303
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
304
        }
305
306
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
307
308
        $checkRequirements = self::retrieveCheckRequirements(
309
            $raw,
310
            null !== $composerJson[0],
311
            null !== $composerLock[0],
312
            false === $isStubGenerated && null === $stubPath,
313
            $logger
314
        );
315
316
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
317
318
        $devPackages = ComposerConfiguration::retrieveDevPackages(
319
            $basePath,
320
            $composerJson[1],
321
            $composerLock[1],
322
            $excludeDevPackages
323
        );
324
325
        /**
326
         * @var string[]
327
         * @var Closure  $blacklistFilter
328
         */
329
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
330
            $raw,
331
            $basePath,
332
            $logger,
333
            $tmpOutputPath,
334
            $outputPath,
335
            $mainScriptPath
336
        );
337
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
338
        // If you consider the following:
339
        //
340
        // {
341
        //   "files": ["file1"],
342
        //   "blacklist": ["file1"],
343
        // }
344
        //
345
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
346
        // and directories but the user should always have the possibility to force his way to include a file.
347
        //
348
        // The exception however, is for the following which is essential for the good functioning of Box
349
        $alwaysExcludedPaths = array_map(
350
            static function (string $excludedPath) use ($basePath): string {
351
                return self::normalizePath($excludedPath, $basePath);
352
            },
353
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath])
354
        );
355
356
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
357
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
358
359
        $filesAggregate = self::collectFiles(
360
            $raw,
361
            $basePath,
362
            $mainScriptPath,
363
            $blacklistFilter,
364
            $excludedPaths,
365
            $alwaysExcludedPaths,
366
            $devPackages,
367
            $composerFiles,
368
            $autodiscoverFiles,
369
            $forceFilesAutodiscovery,
370
            $dumpAutoload,
371
            $logger
372
        );
373
        $binaryFilesAggregate = self::collectBinaryFiles(
374
            $raw,
375
            $basePath,
376
            $blacklistFilter,
377
            $excludedPaths,
378
            $alwaysExcludedPaths,
379
            $devPackages,
380
            $logger
381
        );
382
383
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
384
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
385
386
        $fileMode = self::retrieveFileMode($raw, $logger);
387
388
        $map = self::retrieveMap($raw, $logger);
389
        $fileMapper = new MapFile($basePath, $map);
390
391
        $metadata = self::retrieveMetadata($raw, $logger);
392
393
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
394
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
395
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
396
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
397
398
        $replacements = self::retrieveReplacements($raw, $file, $logger);
399
400
        return new self(
401
            $file,
402
            $alias,
403
            $basePath,
404
            $composerJson,
405
            $composerLock,
406
            $filesAggregate,
407
            $binaryFilesAggregate,
408
            $autodiscoverFiles || $forceFilesAutodiscovery,
409
            $dumpAutoload,
410
            $excludeComposerFiles,
411
            $excludeDevPackages,
412
            $compactors,
413
            $compressionAlgorithm,
414
            $fileMode,
415
            $mainScriptPath,
416
            $mainScriptContents,
417
            $fileMapper,
418
            $metadata,
419
            $tmpOutputPath,
420
            $outputPath,
421
            $privateKeyPassphrase,
422
            $privateKeyPath,
423
            $promptForPrivateKey,
424
            $replacements,
425
            $shebang,
426
            $signingAlgorithm,
427
            $stubBannerContents,
428
            $stubBannerPath,
429
            $stubPath,
430
            $isInterceptsFileFunctions,
431
            $isStubGenerated,
432
            $checkRequirements,
433
            $logger->getWarnings(),
434
            $logger->getRecommendations()
435
        );
436
    }
437
438
    /**
439
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
440
     *                                            path relative to it (the base path)
441
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
442
     *                                            string and the second element its decoded contents as an
443
     *                                            associative array.
444
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
445
     *                                            string and the second element its decoded contents as an
446
     *                                            associative array.
447
     * @param SplFileInfo[] $files                List of files
448
     * @param SplFileInfo[] $binaryFiles          List of binary files
449
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
450
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
451
     *                                            installed.json should be removed from the PHAR
452
     * @param Compactor[]   $compactors           List of file contents compactors
453
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
454
     * @param null|int      $fileMode             File mode in octal form
455
     * @param string        $mainScriptPath       The main script file path
456
     * @param string        $mainScriptContents   The processed content of the main script file
457
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
458
     * @param mixed         $metadata             The PHAR Metadata
459
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
460
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
461
     * @param null|string   $shebang              The shebang line
462
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
463
     * @param null|string   $stubBannerContents   The stub banner comment
464
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
465
     * @param null|string   $stubPath             The PHAR stub file path
466
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
467
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
468
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
469
     *                                            running
470
     * @param string[]      $warnings
471
     * @param string[]      $recommendations
472
     */
473
    private function __construct(
474
        ?string $file,
475
        string $alias,
476
        string $basePath,
477
        array $composerJson,
478
        array $composerLock,
479
        array $files,
480
        array $binaryFiles,
481
        bool $autodiscoveredFiles,
482
        bool $dumpAutoload,
483
        bool $excludeComposerFiles,
484
        bool $excludeDevPackages,
485
        array $compactors,
486
        ?int $compressionAlgorithm,
487
        ?int $fileMode,
488
        ?string $mainScriptPath,
489
        ?string $mainScriptContents,
490
        MapFile $fileMapper,
491
        $metadata,
492
        string $tmpOutputPath,
493
        string $outputPath,
494
        ?string $privateKeyPassphrase,
495
        ?string $privateKeyPath,
496
        bool $promptForPrivateKey,
497
        array $replacements,
498
        ?string $shebang,
499
        int $signingAlgorithm,
500
        ?string $stubBannerContents,
501
        ?string $stubBannerPath,
502
        ?string $stubPath,
503
        bool $isInterceptFileFuncs,
504
        bool $isStubGenerated,
505
        bool $checkRequirements,
506
        array $warnings,
507
        array $recommendations
508
    ) {
509
        Assertion::nullOrInArray(
510
            $compressionAlgorithm,
511
            get_phar_compression_algorithms(),
512
            sprintf(
513
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
514
                implode('", "', array_keys(get_phar_compression_algorithms()))
515
            )
516
        );
517
518
        if (null === $mainScriptPath) {
519
            Assertion::null($mainScriptContents);
520
        } else {
521
            Assertion::notNull($mainScriptContents);
522
        }
523
524
        $this->file = $file;
525
        $this->alias = $alias;
526
        $this->basePath = $basePath;
527
        $this->composerJson = $composerJson;
528
        $this->composerLock = $composerLock;
529
        $this->files = $files;
530
        $this->binaryFiles = $binaryFiles;
531
        $this->autodiscoveredFiles = $autodiscoveredFiles;
532
        $this->dumpAutoload = $dumpAutoload;
533
        $this->excludeComposerFiles = $excludeComposerFiles;
534
        $this->excludeDevFiles = $excludeDevPackages;
535
        $this->compactors = $compactors;
536
        $this->compressionAlgorithm = $compressionAlgorithm;
537
        $this->fileMode = $fileMode;
538
        $this->mainScriptPath = $mainScriptPath;
539
        $this->mainScriptContents = $mainScriptContents;
540
        $this->fileMapper = $fileMapper;
541
        $this->metadata = $metadata;
542
        $this->tmpOutputPath = $tmpOutputPath;
543
        $this->outputPath = $outputPath;
544
        $this->privateKeyPassphrase = $privateKeyPassphrase;
545
        $this->privateKeyPath = $privateKeyPath;
546
        $this->promptForPrivateKey = $promptForPrivateKey;
547
        $this->processedReplacements = $replacements;
548
        $this->shebang = $shebang;
549
        $this->signingAlgorithm = $signingAlgorithm;
550
        $this->stubBannerContents = $stubBannerContents;
551
        $this->stubBannerPath = $stubBannerPath;
552
        $this->stubPath = $stubPath;
553
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
554
        $this->isStubGenerated = $isStubGenerated;
555
        $this->checkRequirements = $checkRequirements;
556
        $this->warnings = $warnings;
557
        $this->recommendations = $recommendations;
558
    }
559
560
    public function export(): string
561
    {
562
        $exportedConfig = clone $this;
563
564
        $basePath = $exportedConfig->basePath;
565
566
        /**
567
         * @param null|SplFileInfo|string $path
568
         */
569
        $normalizePath = static function ($path) use ($basePath): ?string {
570
            if (null === $path) {
571
                return null;
572
            }
573
574
            if ($path instanceof SplFileInfo) {
575
                $path = $path->getPathname();
576
            }
577
578
            return make_path_relative($path, $basePath);
579
        };
580
581
        $normalizeProperty = static function (&$property) use ($normalizePath): void {
582
            $property = $normalizePath($property);
583
        };
584
585
        $normalizeProperty($exportedConfig->file);
586
        $normalizeProperty($exportedConfig->composerJson[0]);
587
        $normalizeProperty($exportedConfig->composerLock[0]);
588
        $normalizeProperty($exportedConfig->mainScriptPath);
589
        $normalizeProperty($exportedConfig->tmpOutputPath);
590
        $normalizeProperty($exportedConfig->outputPath);
591
        $normalizeProperty($exportedConfig->privateKeyPath);
592
        $normalizeProperty($exportedConfig->stubBannerPath);
593
        $normalizeProperty($exportedConfig->stubPath);
594
        $exportedConfig->compressionAlgorithm = array_flip(get_phar_compression_algorithms())[$exportedConfig->compressionAlgorithm ?? Phar::NONE];
595
        $exportedConfig->signingAlgorithm = array_flip(get_phar_signing_algorithms())[$exportedConfig->signingAlgorithm];
596
        $exportedConfig->compactors = array_map('get_class', $exportedConfig->compactors);
597
        $exportedConfig->fileMode = '0'.decoct($exportedConfig->fileMode);
598
599
        $cloner = new VarCloner();
600
        $cloner->setMaxItems(-1);
601
        $cloner->setMaxString(-1);
602
603
        $splInfoCaster = static function (SplFileInfo $fileInfo) use ($normalizePath): array {
604
            return [$normalizePath($fileInfo)];
605
        };
606
607
        $cloner->addCasters([
608
            SplFileInfo::class => $splInfoCaster,
609
            SymfonySplFileInfo::class => $splInfoCaster,
610
        ]);
611
612
        return (new CliDumper())->dump(
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Symfony\Compo...$exportedConfig), true) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
613
            $cloner->cloneVar($exportedConfig),
614
            true
615
        );
616
    }
617
618
    public function getConfigurationFile(): ?string
619
    {
620
        return $this->file;
621
    }
622
623
    public function getAlias(): string
624
    {
625
        return $this->alias;
626
    }
627
628
    public function getBasePath(): string
629
    {
630
        return $this->basePath;
631
    }
632
633
    public function getComposerJson(): ?string
634
    {
635
        return $this->composerJson[0];
636
    }
637
638
    public function getDecodedComposerJsonContents(): ?array
639
    {
640
        return $this->composerJson[1];
641
    }
642
643
    public function getComposerLock(): ?string
644
    {
645
        return $this->composerLock[0];
646
    }
647
648
    public function getDecodedComposerLockContents(): ?array
649
    {
650
        return $this->composerLock[1];
651
    }
652
653
    /**
654
     * @return SplFileInfo[]
655
     */
656
    public function getFiles(): array
657
    {
658
        return $this->files;
659
    }
660
661
    /**
662
     * @return SplFileInfo[]
663
     */
664
    public function getBinaryFiles(): array
665
    {
666
        return $this->binaryFiles;
667
    }
668
669
    public function hasAutodiscoveredFiles(): bool
670
    {
671
        return $this->autodiscoveredFiles;
672
    }
673
674
    public function dumpAutoload(): bool
675
    {
676
        return $this->dumpAutoload;
677
    }
678
679
    public function excludeComposerFiles(): bool
680
    {
681
        return $this->excludeComposerFiles;
682
    }
683
684
    public function excludeDevFiles(): bool
685
    {
686
        return $this->excludeDevFiles;
687
    }
688
689
    /**
690
     * @return Compactor[] the list of compactors
691
     */
692
    public function getCompactors(): array
693
    {
694
        return $this->compactors;
695
    }
696
697
    public function getCompressionAlgorithm(): ?int
698
    {
699
        return $this->compressionAlgorithm;
700
    }
701
702
    public function getFileMode(): ?int
703
    {
704
        return $this->fileMode;
705
    }
706
707
    public function hasMainScript(): bool
708
    {
709
        return null !== $this->mainScriptPath;
710
    }
711
712
    public function getMainScriptPath(): string
713
    {
714
        Assertion::notNull(
715
            $this->mainScriptPath,
716
            'Cannot retrieve the main script path: no main script configured.'
717
        );
718
719
        return $this->mainScriptPath;
720
    }
721
722
    public function getMainScriptContents(): string
723
    {
724
        Assertion::notNull(
725
            $this->mainScriptPath,
726
            'Cannot retrieve the main script contents: no main script configured.'
727
        );
728
729
        return $this->mainScriptContents;
730
    }
731
732
    public function checkRequirements(): bool
733
    {
734
        return $this->checkRequirements;
735
    }
736
737
    public function getTmpOutputPath(): string
738
    {
739
        return $this->tmpOutputPath;
740
    }
741
742
    public function getOutputPath(): string
743
    {
744
        return $this->outputPath;
745
    }
746
747
    public function getFileMapper(): MapFile
748
    {
749
        return $this->fileMapper;
750
    }
751
752
    /**
753
     * @return mixed
754
     */
755
    public function getMetadata()
756
    {
757
        return $this->metadata;
758
    }
759
760
    public function getPrivateKeyPassphrase(): ?string
761
    {
762
        return $this->privateKeyPassphrase;
763
    }
764
765
    public function getPrivateKeyPath(): ?string
766
    {
767
        return $this->privateKeyPath;
768
    }
769
770
    /**
771
     * @deprecated Use promptForPrivateKey() instead
772
     */
773
    public function isPrivateKeyPrompt(): bool
774
    {
775
        return $this->promptForPrivateKey;
776
    }
777
778
    public function promptForPrivateKey(): bool
779
    {
780
        return $this->promptForPrivateKey;
781
    }
782
783
    /**
784
     * @return scalar[]
785
     */
786
    public function getReplacements(): array
787
    {
788
        return $this->processedReplacements;
789
    }
790
791
    public function getShebang(): ?string
792
    {
793
        return $this->shebang;
794
    }
795
796
    public function getSigningAlgorithm(): int
797
    {
798
        return $this->signingAlgorithm;
799
    }
800
801
    public function getStubBannerContents(): ?string
802
    {
803
        return $this->stubBannerContents;
804
    }
805
806
    public function getStubBannerPath(): ?string
807
    {
808
        return $this->stubBannerPath;
809
    }
810
811
    public function getStubPath(): ?string
812
    {
813
        return $this->stubPath;
814
    }
815
816
    public function isInterceptFileFuncs(): bool
817
    {
818
        return $this->isInterceptFileFuncs;
819
    }
820
821
    public function isStubGenerated(): bool
822
    {
823
        return $this->isStubGenerated;
824
    }
825
826
    /**
827
     * @return string[]
828
     */
829
    public function getWarnings(): array
830
    {
831
        return $this->warnings;
832
    }
833
834
    /**
835
     * @return string[]
836
     */
837
    public function getRecommendations(): array
838
    {
839
        return $this->recommendations;
840
    }
841
842
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
843
    {
844
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
845
846
        if (false === isset($raw->{self::ALIAS_KEY})) {
847
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
848
        }
849
850
        $alias = trim($raw->{self::ALIAS_KEY});
851
852
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
853
854
        if ($userStubUsed) {
855
            $logger->addWarning(
856
                sprintf(
857
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
858
                    self::ALIAS_KEY
859
                )
860
            );
861
        }
862
863
        return $alias;
864
    }
865
866
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
867
    {
868
        if (null === $file) {
869
            return getcwd();
870
        }
871
872
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
873
            return realpath(dirname($file));
874
        }
875
876
        $basePath = trim($raw->{self::BASE_PATH_KEY});
877
878
        Assertion::directory(
879
            $basePath,
880
            'The base path "%s" is not a directory or does not exist.'
881
        );
882
883
        $basePath = realpath($basePath);
884
        $defaultPath = realpath(dirname($file));
885
886
        if ($basePath === $defaultPath) {
887
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
888
        }
889
890
        return $basePath;
891
    }
892
893
    /**
894
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
895
     */
896
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
897
    {
898
        if (null === $file) {
899
            return true;
900
        }
901
902
        $associativeRaw = (array) $raw;
903
904
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
905
    }
906
907
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
908
    {
909
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
910
911
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
912
    }
913
914
    private static function retrieveBlacklistFilter(
915
        stdClass $raw,
916
        string $basePath,
917
        ConfigurationLogger $logger,
918
        ?string ...$excludedPaths
919
    ): array {
920
        $blacklist = array_flip(
921
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
922
        );
923
924
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
925
            if ($file->isLink()) {
926
                return false;
927
            }
928
929
            if (false === $file->getRealPath()) {
930
                return false;
931
            }
932
933
            if (array_key_exists($file->getRealPath(), $blacklist)) {
934
                return false;
935
            }
936
937
            return null;
938
        };
939
940
        return [array_keys($blacklist), $blacklistFilter];
941
    }
942
943
    /**
944
     * @param null[]|string[] $excludedPaths
945
     *
946
     * @return string[]
947
     */
948
    private static function retrieveBlacklist(
949
        stdClass $raw,
950
        string $basePath,
951
        ConfigurationLogger $logger,
952
        ?string ...$excludedPaths
953
    ): array {
954
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
955
956
        $normalizedBlacklist = array_map(
957
            static function (string $excludedPath) use ($basePath): string {
958
                return self::normalizePath($excludedPath, $basePath);
959
            },
960
            array_filter($excludedPaths)
961
        );
962
963
        /** @var string[] $blacklist */
964
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
965
966
        foreach ($blacklist as $file) {
967
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
968
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
969
        }
970
971
        return array_unique($normalizedBlacklist);
972
    }
973
974
    /**
975
     * @param string[] $excludedPaths
976
     * @param string[] $alwaysExcludedPaths
977
     * @param string[] $devPackages
978
     *
979
     * @return SplFileInfo[]
980
     */
981
    private static function collectFiles(
982
        stdClass $raw,
983
        string $basePath,
984
        ?string $mainScriptPath,
985
        Closure $blacklistFilter,
986
        array $excludedPaths,
987
        array $alwaysExcludedPaths,
988
        array $devPackages,
989
        array $composerFiles,
990
        bool $autodiscoverFiles,
991
        bool $forceFilesAutodiscovery,
992
        bool $dumpAutoload,
993
        ConfigurationLogger $logger
994
    ): array {
995
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)];
996
997
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
998
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
999
                $basePath,
1000
                $composerFiles[0][1],
1001
                $devPackages,
1002
                array_filter(
1003
                    array_column($composerFiles, 0)
1004
                ),
1005
                $excludedPaths
1006
            );
1007
1008
            $files[] = self::wrapInSplFileInfo($filesToAppend);
1009
1010
            $files[] = self::retrieveAllFiles(
1011
                $basePath,
1012
                $directories,
1013
                $mainScriptPath,
1014
                $blacklistFilter,
1015
                $excludedPaths,
1016
                $devPackages
1017
            );
1018
        }
1019
1020
        if (false === $autodiscoverFiles) {
1021
            $files[] = self::retrieveDirectories(
1022
                $raw,
1023
                self::DIRECTORIES_KEY,
1024
                $basePath,
1025
                $blacklistFilter,
1026
                $excludedPaths,
1027
                $logger
1028
            );
1029
1030
            $filesFromFinders = self::retrieveFilesFromFinders(
1031
                $raw,
1032
                self::FINDER_KEY,
1033
                $basePath,
1034
                $blacklistFilter,
1035
                $devPackages,
1036
                $logger
1037
            );
1038
1039
            foreach ($filesFromFinders as $filesFromFinder) {
1040
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
1041
                $files[] = $filesFromFinder;
1042
            }
1043
        }
1044
1045
        $aggregate = self::retrieveFilesAggregate(...$files);
1046
1047
        if (false === $dumpAutoload) {
1048
            return $aggregate;
1049
        }
1050
1051
        if (null === $composerFiles[1][0]) {
1052
            return $aggregate;
1053
        }
1054
1055
        foreach ($aggregate as $file) {
1056
            if ($file->getRealPath() === $composerFiles[2][0]) {
1057
                return $aggregate;
1058
            }
1059
        }
1060
1061
        $logger->addWarning(
1062
            'A composer.lock file has been found but its related file vendor/composer/installed.json could not. This '
1063
            .'could be due to either dependencies incorrectly installed or an incorrect Box configuration which is not '
1064
            .'including the installed.json file. This will not break the build but will likely result in a broken '
1065
            .'Composer classmap.'
1066
        );
1067
1068
        return $aggregate;
1069
    }
1070
1071
    /**
1072
     * @param string[] $excludedPaths
1073
     * @param string[] $alwaysExcludedPaths
1074
     * @param string[] $devPackages
1075
     *
1076
     * @return SplFileInfo[]
1077
     */
1078
    private static function collectBinaryFiles(
1079
        stdClass $raw,
1080
        string $basePath,
1081
        Closure $blacklistFilter,
1082
        array $excludedPaths,
1083
        array $alwaysExcludedPaths,
1084
        array $devPackages,
1085
        ConfigurationLogger $logger
1086
    ): array {
1087
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $alwaysExcludedPaths, $logger);
1088
1089
        $binaryDirectories = self::retrieveDirectories(
1090
            $raw,
1091
            self::DIRECTORIES_BIN_KEY,
1092
            $basePath,
1093
            $blacklistFilter,
1094
            $excludedPaths,
1095
            $logger
1096
        );
1097
1098
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
1099
            $raw,
1100
            self::FINDER_BIN_KEY,
1101
            $basePath,
1102
            $blacklistFilter,
1103
            $devPackages,
1104
            $logger
1105
        );
1106
1107
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
1108
    }
1109
1110
    /**
1111
     * @param string[] $excludedFiles
1112
     *
1113
     * @return SplFileInfo[]
1114
     */
1115
    private static function retrieveFiles(
1116
        stdClass $raw,
1117
        string $key,
1118
        string $basePath,
1119
        array $composerFiles,
1120
        array $excludedFiles,
1121
        ConfigurationLogger $logger
1122
    ): array {
1123
        self::checkIfDefaultValue($logger, $raw, $key, []);
1124
1125
        $excludedFiles = array_flip($excludedFiles);
1126
        $files = [];
1127
1128
        if (isset($composerFiles[0][0])) {
1129
            $files[] = $composerFiles[0][0];
1130
        }
1131
1132
        if (isset($composerFiles[1][1])) {
1133
            $files[] = $composerFiles[1][0];
1134
        }
1135
1136
        if (false === isset($raw->{$key})) {
1137
            return self::wrapInSplFileInfo($files);
1138
        }
1139
1140
        if ([] === (array) $raw->{$key}) {
1141
            return self::wrapInSplFileInfo($files);
1142
        }
1143
1144
        $files = array_merge((array) $raw->{$key}, $files);
1145
1146
        Assertion::allString($files);
1147
1148
        $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo {
1149
            $file = self::normalizePath($file, $basePath);
1150
1151
            Assertion::false(
1152
                is_link($file),
1153
                sprintf(
1154
                    'Cannot add the link "%s": links are not supported.',
1155
                    $file
1156
                )
1157
            );
1158
1159
            Assertion::file(
1160
                $file,
1161
                sprintf(
1162
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1163
                    $key
1164
                )
1165
            );
1166
1167
            return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file);
1168
        };
1169
1170
        return array_filter(array_map($normalizePath, $files));
1171
    }
1172
1173
    /**
1174
     * @param string   $key           Config property name
1175
     * @param string[] $excludedPaths
1176
     *
1177
     * @return iterable|SplFileInfo[]
1178
     */
1179
    private static function retrieveDirectories(
1180
        stdClass $raw,
1181
        string $key,
1182
        string $basePath,
1183
        Closure $blacklistFilter,
1184
        array $excludedPaths,
1185
        ConfigurationLogger $logger
1186
    ): iterable {
1187
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1188
1189
        if ([] !== $directories) {
1190
            $finder = Finder::create()
1191
                ->files()
1192
                ->filter($blacklistFilter)
1193
                ->ignoreVCS(true)
1194
                ->in($directories)
1195
            ;
1196
1197
            foreach ($excludedPaths as $excludedPath) {
1198
                $finder->notPath($excludedPath);
1199
            }
1200
1201
            return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[]|iterable.
Loading history...
1202
        }
1203
1204
        return [];
1205
    }
1206
1207
    /**
1208
     * @param string[] $devPackages
1209
     *
1210
     * @return iterable[]|SplFileInfo[][]
1211
     */
1212
    private static function retrieveFilesFromFinders(
1213
        stdClass $raw,
1214
        string $key,
1215
        string $basePath,
1216
        Closure $blacklistFilter,
1217
        array $devPackages,
1218
        ConfigurationLogger $logger
1219
    ): array {
1220
        self::checkIfDefaultValue($logger, $raw, $key, []);
1221
1222
        if (false === isset($raw->{$key})) {
1223
            return [];
1224
        }
1225
1226
        $finder = $raw->{$key};
1227
1228
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1229
    }
1230
1231
    /**
1232
     * @param iterable[]|SplFileInfo[][] $fileIterators
1233
     *
1234
     * @return SplFileInfo[]
1235
     */
1236
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1237
    {
1238
        $files = [];
1239
1240
        foreach ($fileIterators as $fileIterator) {
1241
            foreach ($fileIterator as $file) {
1242
                $files[(string) $file] = $file;
1243
            }
1244
        }
1245
1246
        return array_values($files);
1247
    }
1248
1249
    /**
1250
     * @param string[] $devPackages
1251
     *
1252
     * @return Finder[]|SplFileInfo[][]
1253
     */
1254
    private static function processFinders(
1255
        array $findersConfig,
1256
        string $basePath,
1257
        Closure $blacklistFilter,
1258
        array $devPackages
1259
    ): array {
1260
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1261
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1262
        };
1263
1264
        return array_map($processFinderConfig, $findersConfig);
1265
    }
1266
1267
    /**
1268
     * @param string[] $devPackages
1269
     *
1270
     * @return Finder|SplFileInfo[]
1271
     */
1272
    private static function processFinder(
1273
        stdClass $config,
1274
        string $basePath,
1275
        Closure $blacklistFilter,
1276
        array $devPackages
1277
    ): Finder {
1278
        $finder = Finder::create()
1279
            ->files()
1280
            ->filter($blacklistFilter)
1281
            ->filter(
1282
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1283
                    foreach ($devPackages as $devPackage) {
1284
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1285
                            // File belongs to the dev package
1286
                            return false;
1287
                        }
1288
                    }
1289
1290
                    return true;
1291
                }
1292
            )
1293
            ->ignoreVCS(true)
1294
        ;
1295
1296
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1297
            $normalizedConfig = [];
1298
1299
            foreach ($config as $method => $arguments) {
1300
                $method = trim($method);
1301
                $arguments = (array) $arguments;
1302
1303
                Assertion::methodExists(
1304
                    $method,
1305
                    $finder,
1306
                    'The method "Finder::%s" does not exist.'
1307
                );
1308
1309
                $normalizedConfig[$method] = $arguments;
1310
            }
1311
1312
            krsort($normalizedConfig);
1313
1314
            return $normalizedConfig;
1315
        })((array) $config, $finder);
1316
1317
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1318
            $directory = self::normalizePath($directory, $basePath);
1319
1320
            Assertion::false(
1321
                is_link($directory),
1322
                sprintf(
1323
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1324
                    $directory
1325
                )
1326
            );
1327
1328
            Assertion::directory($directory);
1329
1330
            return $directory;
1331
        };
1332
1333
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1334
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1334
            $fileOrDirectory = self::normalizePath(/** @scrutinizer ignore-type */ $fileOrDirectory, $basePath);
Loading history...
1335
1336
            Assertion::false(
1337
                is_link($fileOrDirectory),
1338
                sprintf(
1339
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1340
                    $fileOrDirectory
1341
                )
1342
            );
1343
1344
            Assertion::true(
1345
                file_exists($fileOrDirectory),
1346
                sprintf(
1347
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1348
                    $fileOrDirectory
1349
                )
1350
            );
1351
1352
            if (false === is_file($fileOrDirectory)) {
1353
                Assertion::directory($fileOrDirectory);
1354
            } else {
1355
                Assertion::file($fileOrDirectory);
1356
            }
1357
1358
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1359
                $fileOrDirectory = null;
1360
            }
1361
        };
1362
1363
        foreach ($normalizedConfig as $method => $arguments) {
1364
            if ('in' === $method) {
1365
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1366
            }
1367
1368
            if ('exclude' === $method) {
1369
                $arguments = array_unique(array_map('trim', $arguments));
1370
            }
1371
1372
            if ('append' === $method) {
1373
                array_walk($arguments, $normalizeFileOrDirectory);
1374
1375
                $arguments = [array_filter($arguments)];
1376
            }
1377
1378
            foreach ($arguments as $argument) {
1379
                $finder->$method($argument);
1380
            }
1381
        }
1382
1383
        return $finder;
1384
    }
1385
1386
    /**
1387
     * @param string[] $devPackages
1388
     * @param string[] $filesToAppend
1389
     *
1390
     * @return string[][]
1391
     */
1392
    private static function retrieveAllDirectoriesToInclude(
1393
        string $basePath,
1394
        ?array $decodedJsonContents,
1395
        array $devPackages,
1396
        array $filesToAppend,
1397
        array $excludedPaths
1398
    ): array {
1399
        $toString = static function ($file): string {
1400
            // @param string|SplFileInfo $file
1401
            return (string) $file;
1402
        };
1403
1404
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1405
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1406
        } else {
1407
            $vendorDir = self::normalizePath('vendor', $basePath);
1408
        }
1409
1410
        if (file_exists($vendorDir)) {
1411
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1412
            // dependencies are included in the `composer.json`
1413
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1414
1415
            if (file_exists($installedJsonFiles)) {
1416
                $filesToAppend[] = $installedJsonFiles;
1417
            }
1418
1419
            $vendorPackages = toArray(values(map(
1420
                $toString,
1421
                Finder::create()
1422
                    ->in($vendorDir)
1423
                    ->directories()
1424
                    ->depth(1)
1425
                    ->ignoreUnreadableDirs()
1426
                    ->filter(
1427
                        static function (SplFileInfo $fileInfo): ?bool {
1428
                            if ($fileInfo->isLink()) {
1429
                                return false;
1430
                            }
1431
1432
                            return null;
1433
                        }
1434
                    )
1435
            )));
1436
1437
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1438
1439
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1440
                $files = toArray(values(map(
1441
                    $toString,
1442
                    Finder::create()
1443
                        ->in($basePath)
1444
                        ->files()
1445
                        ->depth(0)
1446
                )));
1447
1448
                $directories = toArray(values(map(
1449
                    $toString,
1450
                    Finder::create()
1451
                        ->in($basePath)
1452
                        ->notPath('vendor')
1453
                        ->directories()
1454
                        ->depth(0)
1455
                )));
1456
1457
                return [
1458
                    array_merge($files, $filesToAppend),
1459
                    array_merge($directories, $vendorPackages),
1460
                ];
1461
            }
1462
1463
            $paths = $vendorPackages;
1464
        } else {
1465
            $paths = [];
1466
        }
1467
1468
        $autoload = $decodedJsonContents['autoload'] ?? [];
1469
1470
        if (array_key_exists('psr-4', $autoload)) {
1471
            foreach ($autoload['psr-4'] as $path) {
1472
                /** @var string|string[] $path */
1473
                $composerPaths = (array) $path;
1474
1475
                foreach ($composerPaths as $composerPath) {
1476
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1477
                }
1478
            }
1479
        }
1480
1481
        if (array_key_exists('psr-0', $autoload)) {
1482
            foreach ($autoload['psr-0'] as $path) {
1483
                /** @var string|string[] $path */
1484
                $composerPaths = (array) $path;
1485
1486
                foreach ($composerPaths as $composerPath) {
1487
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1488
                }
1489
            }
1490
        }
1491
1492
        if (array_key_exists('classmap', $autoload)) {
1493
            foreach ($autoload['classmap'] as $path) {
1494
                // @var string $path
1495
                $paths[] = $path;
1496
            }
1497
        }
1498
1499
        $normalizePath = static function (string $path) use ($basePath): string {
1500
            return is_absolute_path($path)
1501
                ? canonicalize($path)
1502
                : self::normalizePath(trim($path, '/ '), $basePath)
1503
            ;
1504
        };
1505
1506
        if (array_key_exists('files', $autoload)) {
1507
            foreach ($autoload['files'] as $path) {
1508
                // @var string $path
1509
                $path = $normalizePath($path);
1510
1511
                Assertion::file($path);
1512
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1513
1514
                $filesToAppend[] = $path;
1515
            }
1516
        }
1517
1518
        $files = $filesToAppend;
1519
        $directories = [];
1520
1521
        foreach ($paths as $path) {
1522
            $path = $normalizePath($path);
1523
1524
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1525
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1526
1527
            if (is_file($path)) {
1528
                $files[] = $path;
1529
            } else {
1530
                $directories[] = $path;
1531
            }
1532
        }
1533
1534
        [$files, $directories] = [
1535
            array_unique($files),
1536
            array_unique($directories),
1537
        ];
1538
1539
        return [
1540
            array_diff($files, $excludedPaths),
1541
            array_diff($directories, $excludedPaths),
1542
        ];
1543
    }
1544
1545
    /**
1546
     * @param string[] $files
1547
     * @param string[] $directories
1548
     * @param string[] $excludedPaths
1549
     * @param string[] $devPackages
1550
     *
1551
     * @return SplFileInfo[]
1552
     */
1553
    private static function retrieveAllFiles(
1554
        string $basePath,
1555
        array $directories,
1556
        ?string $mainScriptPath,
1557
        Closure $blacklistFilter,
1558
        array $excludedPaths,
1559
        array $devPackages
1560
    ): iterable {
1561
        if ([] === $directories) {
1562
            return [];
1563
        }
1564
1565
        $relativeDevPackages = array_map(
1566
            static function (string $packagePath) use ($basePath): string {
1567
                return make_path_relative($packagePath, $basePath);
1568
            },
1569
            $devPackages
1570
        );
1571
1572
        $finder = Finder::create()
1573
            ->files()
1574
            ->filter($blacklistFilter)
1575
            ->exclude($relativeDevPackages)
1576
            ->ignoreVCS(true)
1577
            ->ignoreDotFiles(true)
1578
            // Remove build files
1579
            ->notName('composer.json')
1580
            ->notName('composer.lock')
1581
            ->notName('Makefile')
1582
            ->notName('Vagrantfile')
1583
            ->notName('phpstan*.neon*')
1584
            ->notName('infection*.json*')
1585
            ->notName('humbug*.json*')
1586
            ->notName('easy-coding-standard.neon*')
1587
            ->notName('phpbench.json*')
1588
            ->notName('phpcs.xml*')
1589
            ->notName('psalm.xml*')
1590
            ->notName('scoper.inc*')
1591
            ->notName('box*.json*')
1592
            ->notName('phpdoc*.xml*')
1593
            ->notName('codecov.yml*')
1594
            ->notName('Dockerfile')
1595
            ->exclude('build')
1596
            ->exclude('dist')
1597
            ->exclude('example')
1598
            ->exclude('examples')
1599
            // Remove documentation
1600
            ->notName('*.md')
1601
            ->notName('*.rst')
1602
            ->notName('/^readme((?!\.php)(\..*+))?$/i')
1603
            ->notName('/^upgrade((?!\.php)(\..*+))?$/i')
1604
            ->notName('/^contributing((?!\.php)(\..*+))?$/i')
1605
            ->notName('/^changelog((?!\.php)(\..*+))?$/i')
1606
            ->notName('/^authors?((?!\.php)(\..*+))?$/i')
1607
            ->notName('/^conduct((?!\.php)(\..*+))?$/i')
1608
            ->notName('/^todo((?!\.php)(\..*+))?$/i')
1609
            ->exclude('doc')
1610
            ->exclude('docs')
1611
            ->exclude('documentation')
1612
            // Remove backup files
1613
            ->notName('*~')
1614
            ->notName('*.back')
1615
            ->notName('*.swp')
1616
            // Remove tests
1617
            ->notName('*Test.php')
1618
            ->exclude('test')
1619
            ->exclude('Test')
1620
            ->exclude('tests')
1621
            ->exclude('Tests')
1622
            ->notName('/phpunit.*\.xml(.dist)?/')
1623
            ->notName('/behat.*\.yml(.dist)?/')
1624
            ->exclude('spec')
1625
            ->exclude('specs')
1626
            ->exclude('features')
1627
            // Remove CI config
1628
            ->exclude('travis')
1629
            ->notName('travis.yml')
1630
            ->notName('appveyor.yml')
1631
            ->notName('build.xml*')
1632
        ;
1633
1634
        if (null !== $mainScriptPath) {
1635
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1636
        }
1637
1638
        $finder->in($directories);
1639
1640
        $excludedPaths = array_unique(
1641
            array_filter(
1642
                array_map(
1643
                    static function (string $path) use ($basePath): string {
1644
                        return make_path_relative($path, $basePath);
1645
                    },
1646
                    $excludedPaths
1647
                ),
1648
                static function (string $path): bool {
1649
                    return '..' !== substr($path, 0, 2);
1650
                }
1651
            )
1652
        );
1653
1654
        foreach ($excludedPaths as $excludedPath) {
1655
            $finder->notPath($excludedPath);
1656
        }
1657
1658
        return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[].
Loading history...
1659
    }
1660
1661
    /**
1662
     * @param string $key Config property name
1663
     *
1664
     * @return string[]
1665
     */
1666
    private static function retrieveDirectoryPaths(
1667
        stdClass $raw,
1668
        string $key,
1669
        string $basePath,
1670
        ConfigurationLogger $logger
1671
    ): array {
1672
        self::checkIfDefaultValue($logger, $raw, $key, []);
1673
1674
        if (false === isset($raw->{$key})) {
1675
            return [];
1676
        }
1677
1678
        $directories = $raw->{$key};
1679
1680
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1681
            $directory = self::normalizePath($directory, $basePath);
1682
1683
            Assertion::false(
1684
                is_link($directory),
1685
                sprintf(
1686
                    'Cannot add the link "%s": links are not supported.',
1687
                    $directory
1688
                )
1689
            );
1690
1691
            Assertion::directory(
1692
                $directory,
1693
                sprintf(
1694
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1695
                    $key
1696
                )
1697
            );
1698
1699
            return $directory;
1700
        };
1701
1702
        return array_map($normalizeDirectory, $directories);
1703
    }
1704
1705
    private static function normalizePath(string $file, string $basePath): string
1706
    {
1707
        return make_path_absolute(trim($file), $basePath);
1708
    }
1709
1710
    /**
1711
     * @param string[] $files
1712
     *
1713
     * @return SplFileInfo[]
1714
     */
1715
    private static function wrapInSplFileInfo(array $files): array
1716
    {
1717
        return array_map(
1718
            static function (string $file): SplFileInfo {
1719
                return new SplFileInfo($file);
1720
            },
1721
            $files
1722
        );
1723
    }
1724
1725
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1726
    {
1727
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1728
1729
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1730
            return $composerJson;
1731
        }
1732
1733
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1734
1735
        if (false === $composerJson && $dumpAutoload) {
1736
            // TODO: use sprintf there; check other occurrences as well
1737
            $logger->addWarning(
1738
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1739
                .' for it could not be found'
1740
            );
1741
1742
            return false;
1743
        }
1744
1745
        return $composerJson && false !== $dumpAutoload;
1746
    }
1747
1748
    private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool
1749
    {
1750
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload);
1751
1752
        if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) {
1753
            return $dumpAutoload;
1754
        }
1755
1756
        $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload;
1757
1758
        if (true === $excludeDevFiles && false === $dumpAutoload) {
1759
            $logger->addWarning(sprintf(
1760
                'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped',
1761
                self::EXCLUDE_DEV_FILES_KEY
1762
            ));
1763
1764
            return false;
1765
        }
1766
1767
        return $excludeDevFiles;
1768
    }
1769
1770
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1771
    {
1772
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1773
1774
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1775
    }
1776
1777
    /**
1778
     * @return Compactor[]
1779
     */
1780
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1781
    {
1782
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1783
1784
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1785
1786
        $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger);
1787
1788
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1789
            return [];
1790
        }
1791
1792
        $compactors = array_map(
1793
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1794
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1795
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1796
1797
                if (LegacyPhp::class === $class) {
1798
                    $logger->addRecommendation(
1799
                        sprintf(
1800
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1801
                            LegacyPhp::class,
1802
                            PhpCompactor::class
1803
                        )
1804
                    );
1805
                }
1806
1807
                if (LegacyJson::class === $class) {
1808
                    $logger->addRecommendation(
1809
                        sprintf(
1810
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1811
                            LegacyJson::class,
1812
                            JsonCompactor::class
1813
                        )
1814
                    );
1815
                }
1816
1817
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1818
                    return self::createPhpCompactor($ignoredAnnotations);
1819
                }
1820
1821
                if (PhpScoperCompactor::class === $class) {
1822
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1823
                }
1824
1825
                return new $class();
1826
            },
1827
            $compactorClasses
1828
        );
1829
1830
        $scoperCompactor = false;
1831
1832
        foreach ($compactors as $compactor) {
1833
            if ($compactor instanceof PhpScoperCompactor) {
1834
                $scoperCompactor = true;
1835
            }
1836
1837
            if ($compactor instanceof PhpCompactor) {
1838
                if (true === $scoperCompactor) {
1839
                    $logger->addRecommendation(
1840
                        sprintf(
1841
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1842
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1843
                        )
1844
                    );
1845
                }
1846
1847
                break;
1848
            }
1849
        }
1850
1851
        return $compactors;
1852
    }
1853
1854
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1855
    {
1856
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1857
1858
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1859
            return null;
1860
        }
1861
1862
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1863
1864
        Assertion::inArray(
1865
            $raw->{self::COMPRESSION_KEY},
1866
            $knownAlgorithmNames,
1867
            sprintf(
1868
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1869
                implode('", "', $knownAlgorithmNames)
1870
            )
1871
        );
1872
1873
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1874
1875
        // Phar::NONE is not valid for compressFiles()
1876
        if (Phar::NONE === $value) {
1877
            return null;
1878
        }
1879
1880
        return $value;
1881
    }
1882
1883
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1884
    {
1885
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1886
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1887
        }
1888
1889
        $defaultChmod = intval(0755, 8);
1890
1891
        if (isset($raw->{self::CHMOD_KEY})) {
1892
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1893
1894
            if ($defaultChmod === $chmod) {
1895
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1896
            }
1897
1898
            return $chmod;
1899
        }
1900
1901
        return $defaultChmod;
1902
    }
1903
1904
    private static function retrieveMainScriptPath(
1905
        stdClass $raw,
1906
        string $basePath,
1907
        ?array $decodedJsonContents,
1908
        ConfigurationLogger $logger
1909
    ): ?string {
1910
        $firstBin = false;
1911
1912
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1913
            /** @var false|string $firstBin */
1914
            $firstBin = current((array) $decodedJsonContents['bin']);
1915
1916
            if (false !== $firstBin) {
1917
                $firstBin = self::normalizePath($firstBin, $basePath);
1918
            }
1919
        }
1920
1921
        if (isset($raw->{self::MAIN_KEY})) {
1922
            $main = $raw->{self::MAIN_KEY};
1923
1924
            if (is_string($main)) {
1925
                $main = self::normalizePath($main, $basePath);
1926
1927
                if ($main === $firstBin) {
1928
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1929
                }
1930
            }
1931
        } else {
1932
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1933
        }
1934
1935
        if (is_bool($main)) {
1936
            Assertion::false(
1937
                $main,
1938
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1939
            );
1940
1941
            return null;
1942
        }
1943
1944
        Assertion::file($main);
1945
1946
        return $main;
1947
    }
1948
1949
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1950
    {
1951
        if (null === $mainScriptPath) {
1952
            return null;
1953
        }
1954
1955
        $contents = file_contents($mainScriptPath);
1956
1957
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1958
        // PHAR entry point file.
1959
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1960
        // case the main script can be ignored or disabled.
1961
        return preg_replace('/^#!.*\s*/', '', $contents);
1962
    }
1963
1964
    /**
1965
     * @return string|null[][]
1966
     */
1967
    private static function retrieveComposerFiles(string $basePath): array
1968
    {
1969
        $retrieveFileAndContents = static function (string $file): array {
1970
            $json = new Json();
1971
1972
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1973
                return [null, null];
1974
            }
1975
1976
            try {
1977
                $contents = $json->decodeFile($file, true);
1978
            } catch (ParsingException $exception) {
1979
                throw new InvalidArgumentException(
1980
                    sprintf(
1981
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1982
                        $file,
1983
                        $exception->getMessage()
1984
                    ),
1985
                    0,
1986
                    $exception
1987
                );
1988
            }
1989
1990
            return [$file, $contents];
1991
        };
1992
1993
        return [
1994
            $retrieveFileAndContents(canonicalize($basePath.'/composer.json')),
1995
            $retrieveFileAndContents(canonicalize($basePath.'/composer.lock')),
1996
            $retrieveFileAndContents(canonicalize($basePath.'/vendor/composer/installed.json')),
1997
        ];
1998
    }
1999
2000
    /**
2001
     * @return string[][]
2002
     */
2003
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
2004
    {
2005
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
2006
2007
        if (false === isset($raw->{self::MAP_KEY})) {
2008
            return [];
2009
        }
2010
2011
        $map = [];
2012
        $rawMap = (array) $raw->{self::MAP_KEY};
2013
2014
        foreach ($rawMap as $item) {
2015
            $processed = [];
2016
2017
            foreach ($item as $match => $replace) {
2018
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
2019
            }
2020
2021
            if (isset($processed['_empty_'])) {
2022
                $processed[''] = $processed['_empty_'];
2023
2024
                unset($processed['_empty_']);
2025
            }
2026
2027
            $map[] = $processed;
2028
        }
2029
2030
        return $map;
2031
    }
2032
2033
    /**
2034
     * @return mixed
2035
     */
2036
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
2037
    {
2038
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
2039
2040
        if (false === isset($raw->{self::METADATA_KEY})) {
2041
            return null;
2042
        }
2043
2044
        $metadata = $raw->{self::METADATA_KEY};
2045
2046
        return is_object($metadata) ? (array) $metadata : $metadata;
2047
    }
2048
2049
    /**
2050
     * @return string[] The first element is the temporary output path and the second the final one
2051
     */
2052
    private static function retrieveOutputPath(
2053
        stdClass $raw,
2054
        string $basePath,
2055
        ?string $mainScriptPath,
2056
        ConfigurationLogger $logger
2057
    ): array {
2058
        $defaultPath = null;
2059
2060
        if (null !== $mainScriptPath
2061
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
2062
        ) {
2063
            $defaultPath = $matches['main'].'.phar';
2064
        }
2065
2066
        if (isset($raw->{self::OUTPUT_KEY})) {
2067
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
2068
2069
            if ($path === $defaultPath) {
2070
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
2071
            }
2072
        } elseif (null !== $defaultPath) {
2073
            $path = $defaultPath;
2074
        } else {
2075
            // Last resort, should not happen
2076
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
2077
        }
2078
2079
        $tmp = $real = $path;
2080
2081
        if ('.phar' !== substr($real, -5)) {
2082
            $tmp .= '.phar';
2083
        }
2084
2085
        return [$tmp, $real];
2086
    }
2087
2088
    private static function retrievePrivateKeyPath(
2089
        stdClass $raw,
2090
        string $basePath,
2091
        int $signingAlgorithm,
2092
        ConfigurationLogger $logger
2093
    ): ?string {
2094
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
2095
            if (null === $raw->{self::KEY_KEY}) {
2096
                $logger->addRecommendation(
2097
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
2098
                );
2099
            } else {
2100
                $logger->addWarning(
2101
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
2102
                );
2103
            }
2104
2105
            return null;
2106
        }
2107
2108
        if (!isset($raw->{self::KEY_KEY})) {
2109
            Assertion::true(
2110
                Phar::OPENSSL !== $signingAlgorithm,
2111
                'Expected to have a private key for OpenSSL signing but none have been provided.'
2112
            );
2113
2114
            return null;
2115
        }
2116
2117
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
2118
2119
        Assertion::file($path);
2120
2121
        return $path;
2122
    }
2123
2124
    private static function retrievePrivateKeyPassphrase(
2125
        stdClass $raw,
2126
        int $algorithm,
2127
        ConfigurationLogger $logger
2128
    ): ?string {
2129
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
2130
2131
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
2132
            return null;
2133
        }
2134
2135
        /** @var null|false|string $keyPass */
2136
        $keyPass = $raw->{self::KEY_PASS_KEY};
2137
2138
        if (Phar::OPENSSL !== $algorithm) {
2139
            if (false === $keyPass || null === $keyPass) {
2140
                $logger->addRecommendation(
2141
                    sprintf(
2142
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
2143
                        .'not "OPENSSL".',
2144
                        self::KEY_PASS_KEY
2145
                    )
2146
                );
2147
            } else {
2148
                $logger->addWarning(
2149
                    sprintf(
2150
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
2151
                        self::KEY_PASS_KEY
2152
                    )
2153
                );
2154
            }
2155
2156
            return null;
2157
        }
2158
2159
        return is_string($keyPass) ? $keyPass : null;
2160
    }
2161
2162
    /**
2163
     * @return scalar[]
2164
     */
2165
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
2166
    {
2167
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
2168
2169
        if (null === $file) {
2170
            return [];
2171
        }
2172
2173
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
2174
2175
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
2176
            $replacements[$git] = self::retrievePrettyGitTag($file);
2177
        }
2178
2179
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
2180
            $replacements[$git] = self::retrieveGitHash($file);
2181
        }
2182
2183
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
2184
            $replacements[$git] = self::retrieveGitHash($file, true);
2185
        }
2186
2187
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
2188
            $replacements[$git] = self::retrieveGitTag($file);
2189
        }
2190
2191
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
2192
            $replacements[$git] = self::retrieveGitVersion($file);
2193
        }
2194
2195
        /**
2196
         * @var string
2197
         * @var bool   $valueSetByUser
2198
         */
2199
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2200
2201
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2202
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2203
        } elseif ($valueSetByUser) {
2204
            $logger->addRecommendation(
2205
                sprintf(
2206
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2207
                    self::DATETIME_FORMAT_KEY,
2208
                    self::DATETIME_KEY
2209
                )
2210
            );
2211
        }
2212
2213
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2214
2215
        foreach ($replacements as $key => $value) {
2216
            unset($replacements[$key]);
2217
            $replacements[$sigil.$key.$sigil] = $value;
2218
        }
2219
2220
        return $replacements;
2221
    }
2222
2223
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2224
    {
2225
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2226
    }
2227
2228
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2229
    {
2230
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2231
    }
2232
2233
    /**
2234
     * @param bool $short Use the short version
2235
     *
2236
     * @return string the commit hash
2237
     */
2238
    private static function retrieveGitHash(string $file, bool $short = false): string
2239
    {
2240
        return self::runGitCommand(
2241
            sprintf(
2242
                'git log --pretty="%s" -n1 HEAD',
2243
                $short ? '%h' : '%H'
2244
            ),
2245
            $file
2246
        );
2247
    }
2248
2249
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2250
    {
2251
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2252
    }
2253
2254
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2255
    {
2256
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2257
    }
2258
2259
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2260
    {
2261
        self::checkIfDefaultValue($logger, $raw, $key);
2262
2263
        return $raw->{$key} ?? null;
2264
    }
2265
2266
    private static function retrieveGitTag(string $file): string
2267
    {
2268
        return self::runGitCommand('git describe --tags HEAD', $file);
2269
    }
2270
2271
    private static function retrievePrettyGitTag(string $file): string
2272
    {
2273
        $version = self::retrieveGitTag($file);
2274
2275
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2276
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2277
        }
2278
2279
        return $version;
2280
    }
2281
2282
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2283
    {
2284
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2285
    }
2286
2287
    private static function retrieveGitVersion(string $file): ?string
2288
    {
2289
        try {
2290
            return self::retrieveGitTag($file);
2291
        } catch (RuntimeException $exception) {
2292
            try {
2293
                return self::retrieveGitHash($file, true);
2294
            } catch (RuntimeException $exception) {
2295
                throw new RuntimeException(
2296
                    sprintf(
2297
                        'The tag or commit hash could not be retrieved from "%s": %s',
2298
                        dirname($file),
2299
                        $exception->getMessage()
2300
                    ),
2301
                    0,
2302
                    $exception
2303
                );
2304
            }
2305
        }
2306
    }
2307
2308
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2309
    {
2310
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2311
    }
2312
2313
    private static function retrieveDatetimeNow(string $format): string
2314
    {
2315
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2316
2317
        return $now->format($format);
2318
    }
2319
2320
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2321
    {
2322
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2323
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2324
2325
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2326
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2327
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2328
            @trigger_error(
2329
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2330
                E_USER_DEPRECATED
2331
            );
2332
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2333
2334
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2335
        } else {
2336
            $format = null;
2337
        }
2338
2339
        if (null !== $format) {
2340
            $formattedDate = (new DateTimeImmutable())->format($format);
2341
2342
            Assertion::false(
2343
                false === $formattedDate || $formattedDate === $format,
2344
                sprintf(
2345
                    'Expected the datetime format to be a valid format: "%s" is not',
2346
                    $format
2347
                )
2348
            );
2349
2350
            return [$format, true];
2351
        }
2352
2353
        return [self::DEFAULT_DATETIME_FORMAT, false];
2354
    }
2355
2356
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2357
    {
2358
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2359
    }
2360
2361
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2362
    {
2363
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2364
2365
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2366
            return self::DEFAULT_SHEBANG;
2367
        }
2368
2369
        $shebang = $raw->{self::SHEBANG_KEY};
2370
2371
        if (false === $shebang) {
2372
            if (false === $stubIsGenerated) {
2373
                $logger->addRecommendation(
2374
                    sprintf(
2375
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2376
                        .' being used',
2377
                        self::SHEBANG_KEY
2378
                    )
2379
                );
2380
            }
2381
2382
            return null;
2383
        }
2384
2385
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2386
2387
        $shebang = trim($shebang);
2388
2389
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2390
        Assertion::true(
2391
            '#!' === substr($shebang, 0, 2),
2392
            sprintf(
2393
                'The shebang line must start with "#!". Got "%s" instead',
2394
                $shebang
2395
            )
2396
        );
2397
2398
        if (false === $stubIsGenerated) {
2399
            $logger->addWarning(
2400
                sprintf(
2401
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2402
                    .' used',
2403
                    self::SHEBANG_KEY
2404
                )
2405
            );
2406
        }
2407
2408
        return $shebang;
2409
    }
2410
2411
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2412
    {
2413
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2414
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2415
        }
2416
2417
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2418
            return self::DEFAULT_SIGNING_ALGORITHM;
2419
        }
2420
2421
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2422
2423
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2424
2425
        Assertion::true(
2426
            defined('Phar::'.$algorithm),
2427
            sprintf(
2428
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2429
                $algorithm
2430
            )
2431
        );
2432
2433
        $algorithm = constant('Phar::'.$algorithm);
2434
2435
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2436
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2437
        }
2438
2439
        return $algorithm;
2440
    }
2441
2442
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2443
    {
2444
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner());
2445
2446
        if (false === isset($raw->{self::BANNER_KEY})) {
2447
            return self::getDefaultBanner();
2448
        }
2449
2450
        $banner = $raw->{self::BANNER_KEY};
2451
2452
        if (false === $banner) {
2453
            if (false === $stubIsGenerated) {
2454
                $logger->addRecommendation(
2455
                    sprintf(
2456
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2457
                        .'being used',
2458
                        self::BANNER_KEY
2459
                    )
2460
                );
2461
            }
2462
2463
            return null;
2464
        }
2465
2466
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2467
2468
        if (is_array($banner)) {
2469
            $banner = implode("\n", $banner);
2470
        }
2471
2472
        if (false === $stubIsGenerated) {
2473
            $logger->addWarning(
2474
                sprintf(
2475
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2476
                    self::BANNER_KEY
2477
                )
2478
            );
2479
        }
2480
2481
        return $banner;
2482
    }
2483
2484
    private static function getDefaultBanner(): string
2485
    {
2486
        return sprintf(self::DEFAULT_BANNER, get_box_version());
2487
    }
2488
2489
    private static function retrieveStubBannerPath(
2490
        stdClass $raw,
2491
        string $basePath,
2492
        bool $stubIsGenerated,
2493
        ConfigurationLogger $logger
2494
    ): ?string {
2495
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2496
2497
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2498
            return null;
2499
        }
2500
2501
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2502
2503
        Assertion::file($bannerFile);
2504
2505
        if (false === $stubIsGenerated) {
2506
            $logger->addWarning(
2507
                sprintf(
2508
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2509
                    self::BANNER_FILE_KEY
2510
                )
2511
            );
2512
        }
2513
2514
        return $bannerFile;
2515
    }
2516
2517
    private static function normalizeStubBannerContents(?string $contents): ?string
2518
    {
2519
        if (null === $contents) {
2520
            return null;
2521
        }
2522
2523
        $banner = explode("\n", $contents);
2524
        $banner = array_map('trim', $banner);
2525
2526
        return implode("\n", $banner);
2527
    }
2528
2529
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2530
    {
2531
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2532
2533
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2534
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2535
2536
            Assertion::file($stubPath);
2537
2538
            return $stubPath;
2539
        }
2540
2541
        return null;
2542
    }
2543
2544
    private static function retrieveInterceptsFileFunctions(
2545
        stdClass $raw,
2546
        bool $stubIsGenerated,
2547
        ConfigurationLogger $logger
2548
    ): bool {
2549
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2550
2551
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2552
            return false;
2553
        }
2554
2555
        $intercept = $raw->{self::INTERCEPT_KEY};
2556
2557
        if ($intercept && 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::INTERCEPT_KEY
2562
                )
2563
            );
2564
        }
2565
2566
        return $intercept;
2567
    }
2568
2569
    private static function retrievePromptForPrivateKey(
2570
        stdClass $raw,
2571
        int $signingAlgorithm,
2572
        ConfigurationLogger $logger
2573
    ): bool {
2574
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2575
            if (Phar::OPENSSL !== $signingAlgorithm) {
2576
                $logger->addWarning(
2577
                    'A prompt for password for the private key has been requested but ignored since the signing '
2578
                    .'algorithm used is not "OPENSSL.'
2579
                );
2580
2581
                return false;
2582
            }
2583
2584
            return true;
2585
        }
2586
2587
        return false;
2588
    }
2589
2590
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2591
    {
2592
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2593
2594
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2595
    }
2596
2597
    private static function retrieveCheckRequirements(
2598
        stdClass $raw,
2599
        bool $hasComposerJson,
2600
        bool $hasComposerLock,
2601
        bool $pharStubUsed,
2602
        ConfigurationLogger $logger
2603
    ): bool {
2604
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2605
2606
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2607
            return $hasComposerJson || $hasComposerLock;
2608
        }
2609
2610
        /** @var bool $checkRequirements */
2611
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2612
2613
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2614
            $logger->addWarning(
2615
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2616
                .'be found.'
2617
            );
2618
2619
            return false;
2620
        }
2621
2622
        if ($checkRequirements && $pharStubUsed) {
2623
            $logger->addWarning(
2624
                sprintf(
2625
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2626
                    .'used.',
2627
                    self::CHECK_REQUIREMENTS_KEY
2628
                )
2629
            );
2630
        }
2631
2632
        return $checkRequirements;
2633
    }
2634
2635
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2636
    {
2637
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2638
2639
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2640
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2641
2642
            return file_exists($configFilePath)
2643
                ? PhpScoperConfiguration::load($configFilePath)
2644
                : PhpScoperConfiguration::load()
2645
             ;
2646
        }
2647
2648
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2649
2650
        Assertion::string($configFile);
2651
2652
        $configFilePath = make_path_absolute($configFile, $basePath);
2653
2654
        Assertion::file($configFilePath);
2655
        Assertion::readable($configFilePath);
2656
2657
        return PhpScoperConfiguration::load($configFilePath);
2658
    }
2659
2660
    /**
2661
     * Runs a Git command on the repository.
2662
     *
2663
     * @return string The trimmed output from the command
2664
     */
2665
    private static function runGitCommand(string $command, string $file): string
2666
    {
2667
        $path = dirname($file);
2668
2669
        $process = Process::fromShellCommandline($command, $path);
2670
2671
        if (0 === $process->run()) {
2672
            return trim($process->getOutput());
2673
        }
2674
2675
        throw new RuntimeException(
2676
            sprintf(
2677
                'The tag or commit hash could not be retrieved from "%s": %s',
2678
                $path,
2679
                $process->getErrorOutput()
2680
            )
2681
        );
2682
    }
2683
2684
    /**
2685
     * @param string[] $compactorClasses
2686
     *
2687
     * @return string[]
2688
     */
2689
    private static function retrievePhpCompactorIgnoredAnnotations(
2690
        stdClass $raw,
2691
        array $compactorClasses,
2692
        ConfigurationLogger $logger
2693
    ): array {
2694
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true) || in_array(LegacyPhp::class, $compactorClasses, true);
2695
2696
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2697
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2698
2699
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2700
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2701
        }
2702
2703
        if (false === $hasPhpCompactor) {
2704
            $logger->addWarning(
2705
                sprintf(
2706
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2707
                    self::ANNOTATIONS_KEY
2708
                )
2709
            );
2710
        }
2711
2712
        /** @var null|bool|stdClass $annotations */
2713
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2714
2715
        if (true === $annotations || null === $annotations) {
2716
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2717
        }
2718
2719
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2720
            return [];
2721
        }
2722
2723
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2724
            $logger->addWarning(
2725
                sprintf(
2726
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2727
                    .' if it is set to `false`',
2728
                    self::ANNOTATIONS_KEY,
2729
                    self::IGNORED_ANNOTATIONS_KEY,
2730
                    self::ANNOTATIONS_KEY
2731
                )
2732
            );
2733
2734
            return [];
2735
        }
2736
2737
        $ignored = [];
2738
2739
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2740
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2741
        ) {
2742
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2743
2744
            return (array) $ignored;
2745
        }
2746
2747
        return $ignored;
2748
    }
2749
2750
    private static function createPhpCompactor(array $ignoredAnnotations): Compactor
2751
    {
2752
        $ignoredAnnotations = array_values(
2753
            array_filter(
2754
                array_map(
2755
                    static function (string $annotation): ?string {
2756
                        return strtolower(trim($annotation));
2757
                    },
2758
                    $ignoredAnnotations
2759
                )
2760
            )
2761
        );
2762
2763
        return new PhpCompactor(
2764
            new DocblockAnnotationParser(
2765
                new DocblockParser(),
2766
                new AnnotationDumper(),
2767
                $ignoredAnnotations
2768
            )
2769
        );
2770
    }
2771
2772
    private static function createPhpScoperCompactor(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactor
2773
    {
2774
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2775
2776
        $phpScoper = (new class() extends ApplicationFactory {
2777
            public static function createScoper(): Scoper
2778
            {
2779
                return parent::createScoper();
2780
            }
2781
        })::createScoper();
2782
2783
        if ([] !== $phpScoperConfig->getWhitelistedFiles()) {
2784
            $whitelistedFiles = array_values(
2785
                array_unique(
2786
                    array_map(
2787
                        static function (string $path) use ($basePath): string {
2788
                            return make_path_relative($path, $basePath);
2789
                        },
2790
                        $phpScoperConfig->getWhitelistedFiles()
2791
                    )
2792
                )
2793
            );
2794
2795
            $phpScoper = new FileWhitelistScoper($phpScoper, ...$whitelistedFiles);
2796
        }
2797
2798
        $prefix = $phpScoperConfig->getPrefix() ?? unique_id('_HumbugBox');
2799
2800
        return new PhpScoperCompactor(
2801
            new SimpleScoper(
2802
                $phpScoper,
2803
                $prefix,
2804
                $phpScoperConfig->getWhitelist(),
2805
                $phpScoperConfig->getPatchers()
2806
            )
2807
        );
2808
    }
2809
2810
    private static function checkIfDefaultValue(
2811
        ConfigurationLogger $logger,
2812
        stdClass $raw,
2813
        string $key,
2814
        $defaultValue = null
2815
    ): void {
2816
        if (false === property_exists($raw, $key)) {
2817
            return;
2818
        }
2819
2820
        $value = $raw->{$key};
2821
2822
        if (null === $value
2823
            || (false === is_object($defaultValue) && $defaultValue === $value)
2824
            || (is_object($defaultValue) && $defaultValue == $value)
2825
        ) {
2826
            $logger->addRecommendation(
2827
                sprintf(
2828
                    'The "%s" setting can be omitted since is set to its default value',
2829
                    $key
2830
                )
2831
            );
2832
        }
2833
    }
2834
2835
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2836
    {
2837
        $logger->addRecommendation(
2838
            sprintf(
2839
                'The "%s" setting can be omitted since is set to its default value',
2840
                $key
2841
            )
2842
        );
2843
    }
2844
}
2845