Passed
Push — master ( d8a9f3...906de1 )
by Théo
02:24
created

src/Configuration/Configuration.php (3 issues)

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