Passed
Pull Request — master (#371)
by Théo
02:14
created

Configuration::retrieveDumpAutoload()   C

Complexity

Conditions 13
Paths 136

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 46
rs 6.3166
c 0
b 0
f 0
cc 13
nc 136
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
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 File;
38
use function file_exists;
39
use function getcwd;
40
use Herrera\Box\Compactor\Json as LegacyJson;
41
use Herrera\Box\Compactor\Php as LegacyPhp;
42
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
43
use Humbug\PhpScoper\Console\ApplicationFactory;
44
use Humbug\PhpScoper\Scoper;
45
use Humbug\PhpScoper\Scoper\FileWhitelistScoper;
46
use function implode;
47
use function in_array;
48
use function intval;
49
use InvalidArgumentException;
50
use function is_array;
51
use function is_bool;
52
use function is_file;
53
use function is_link;
54
use function is_object;
55
use function is_readable;
56
use function is_string;
57
use function iter\map;
58
use function iter\toArray;
59
use function iter\values;
60
use KevinGH\Box\Annotation\AnnotationDumper;
61
use KevinGH\Box\Annotation\DocblockAnnotationParser;
62
use KevinGH\Box\Annotation\DocblockParser;
63
use KevinGH\Box\Compactor\Json as JsonCompactor;
64
use KevinGH\Box\Compactor\Php as PhpCompactor;
65
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
66
use KevinGH\Box\Composer\ComposerConfiguration;
67
use KevinGH\Box\Composer\ComposerFile;
68
use KevinGH\Box\Composer\ComposerFiles;
69
use function KevinGH\Box\FileSystem\canonicalize;
70
use function KevinGH\Box\FileSystem\file_contents;
71
use function KevinGH\Box\FileSystem\is_absolute_path;
72
use function KevinGH\Box\FileSystem\longest_common_base_path;
73
use function KevinGH\Box\FileSystem\make_path_absolute;
74
use function KevinGH\Box\FileSystem\make_path_relative;
75
use KevinGH\Box\Json\Json;
76
use KevinGH\Box\PhpScoper\SimpleScoper;
77
use function krsort;
78
use Phar;
79
use function preg_match;
80
use function preg_replace;
81
use function property_exists;
82
use function realpath;
83
use RuntimeException;
84
use Seld\JsonLint\ParsingException;
85
use function sort;
86
use const SORT_STRING;
87
use SplFileInfo;
88
use function sprintf;
89
use stdClass;
90
use function strtoupper;
91
use function substr;
92
use Symfony\Component\Finder\Finder;
93
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
94
use Symfony\Component\Process\Process;
95
use Symfony\Component\VarDumper\Cloner\VarCloner;
96
use Symfony\Component\VarDumper\Dumper\CliDumper;
97
use function trigger_error;
98
use function trim;
99
100
/**
101
 * @private
102
 */
103
final class Configuration
104
{
105
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
106
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
107
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
108
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
109
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
110
    private const DEFAULT_BANNER = <<<'BANNER'
111
Generated by Humbug Box %s.
112
113
@link https://github.com/humbug/box
114
BANNER;
115
    private const FILES_SETTINGS = [
116
        'directories',
117
        'finder',
118
    ];
119
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
120
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
121
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
122
123
    private const DEFAULT_IGNORED_ANNOTATIONS = [
124
        'abstract',
125
        'access',
126
        'annotation',
127
        'api',
128
        'attribute',
129
        'attributes',
130
        'author',
131
        'category',
132
        'code',
133
        'codecoverageignore',
134
        'codecoverageignoreend',
135
        'codecoverageignorestart',
136
        'copyright',
137
        'deprec',
138
        'deprecated',
139
        'endcode',
140
        'example',
141
        'exception',
142
        'filesource',
143
        'final',
144
        'fixme',
145
        'global',
146
        'ignore',
147
        'ingroup',
148
        'inheritdoc',
149
        'internal',
150
        'license',
151
        'link',
152
        'magic',
153
        'method',
154
        'name',
155
        'override',
156
        'package',
157
        'package_version',
158
        'param',
159
        'private',
160
        'property',
161
        'required',
162
        'return',
163
        'see',
164
        'since',
165
        'static',
166
        'staticvar',
167
        'subpackage',
168
        'suppresswarnings',
169
        'target',
170
        'throw',
171
        'throws',
172
        'todo',
173
        'tutorial',
174
        'usedby',
175
        'uses',
176
        'var',
177
        'version',
178
    ];
179
180
    private const ALGORITHM_KEY = 'algorithm';
181
    private const ALIAS_KEY = 'alias';
182
    private const ANNOTATIONS_KEY = 'annotations';
183
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
184
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
185
    private const BANNER_KEY = 'banner';
186
    private const BANNER_FILE_KEY = 'banner-file';
187
    private const BASE_PATH_KEY = 'base-path';
188
    private const BLACKLIST_KEY = 'blacklist';
189
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
190
    private const CHMOD_KEY = 'chmod';
191
    private const COMPACTORS_KEY = 'compactors';
192
    private const COMPRESSION_KEY = 'compression';
193
    private const DATETIME_KEY = 'datetime';
194
    private const DATETIME_FORMAT_KEY = 'datetime-format';
195
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
196
    private const DIRECTORIES_KEY = 'directories';
197
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
198
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
199
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
200
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
201
    private const FILES_KEY = 'files';
202
    private const FILES_BIN_KEY = 'files-bin';
203
    private const FINDER_KEY = 'finder';
204
    private const FINDER_BIN_KEY = 'finder-bin';
205
    private const GIT_KEY = 'git';
206
    private const GIT_COMMIT_KEY = 'git-commit';
207
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
208
    private const GIT_TAG_KEY = 'git-tag';
209
    private const GIT_VERSION_KEY = 'git-version';
210
    private const INTERCEPT_KEY = 'intercept';
211
    private const KEY_KEY = 'key';
212
    private const KEY_PASS_KEY = 'key-pass';
213
    private const MAIN_KEY = 'main';
214
    private const MAP_KEY = 'map';
215
    private const METADATA_KEY = 'metadata';
216
    private const OUTPUT_KEY = 'output';
217
    private const PHP_SCOPER_KEY = 'php-scoper';
218
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
219
    private const REPLACEMENTS_KEY = 'replacements';
220
    private const SHEBANG_KEY = 'shebang';
221
    private const STUB_KEY = 'stub';
222
223
    private $file;
224
    private $fileMode;
225
    private $alias;
226
    private $basePath;
227
    private $composerJson;
228
    private $composerLock;
229
    private $files;
230
    private $binaryFiles;
231
    private $autodiscoveredFiles;
232
    private $dumpAutoload;
233
    private $excludeComposerFiles;
234
    private $excludeDevFiles;
235
    private $compactors;
236
    private $compressionAlgorithm;
237
    private $mainScriptPath;
238
    private $mainScriptContents;
239
    private $fileMapper;
240
    private $metadata;
241
    private $tmpOutputPath;
242
    private $outputPath;
243
    private $privateKeyPassphrase;
244
    private $privateKeyPath;
245
    private $promptForPrivateKey;
246
    private $processedReplacements;
247
    private $shebang;
248
    private $signingAlgorithm;
249
    private $stubBannerContents;
250
    private $stubBannerPath;
251
    private $stubPath;
252
    private $isInterceptFileFuncs;
253
    private $isStubGenerated;
254
    private $checkRequirements;
255
    private $warnings;
256
    private $recommendations;
257
258
    public static function create(?string $file, stdClass $raw): self
259
    {
260
        $logger = new ConfigurationLogger();
261
262
        $basePath = self::retrieveBasePath($file, $raw, $logger);
263
264
        $composerFiles = self::retrieveComposerFiles($basePath);
265
266
        $dumpAutoload = self::retrieveDumpAutoload($raw, $composerFiles, $logger);
267
268
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
269
270
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles->getComposerJson()->getDecodedContents(), $logger);
271
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
272
273
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
274
275
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
276
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
277
278
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
279
280
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
281
282
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
283
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
284
285
        if (null !== $stubBannerPath) {
286
            $stubBannerContents = file_contents($stubBannerPath);
287
        }
288
289
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
290
291
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
292
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
293
        }
294
295
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
296
297
        $checkRequirements = self::retrieveCheckRequirements(
298
            $raw,
299
            null !== $composerFiles->getComposerJson()->getPath(),
300
            null !== $composerFiles->getComposerLock()->getPath(),
301
            false === $isStubGenerated && null === $stubPath,
302
            $logger
303
        );
304
305
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
306
307
        $devPackages = ComposerConfiguration::retrieveDevPackages(
308
            $basePath,
309
            $composerFiles->getComposerJson()->getDecodedContents(),
310
            $composerFiles->getComposerLock()->getDecodedContents(),
311
            $excludeDevPackages
312
        );
313
314
        /**
315
         * @var string[]
316
         * @var Closure  $blacklistFilter
317
         */
318
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
319
            $raw,
320
            $basePath,
321
            $logger,
322
            $tmpOutputPath,
323
            $outputPath,
324
            $mainScriptPath
325
        );
326
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
327
        // If you consider the following:
328
        //
329
        // {
330
        //   "files": ["file1"],
331
        //   "blacklist": ["file1"],
332
        // }
333
        //
334
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
335
        // and directories but the user should always have the possibility to force his way to include a file.
336
        //
337
        // The exception however, is for the following which is essential for the good functioning of Box
338
        $alwaysExcludedPaths = array_map(
339
            static function (string $excludedPath) use ($basePath): string {
340
                return self::normalizePath($excludedPath, $basePath);
341
            },
342
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath])
343
        );
344
345
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
346
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
347
348
        $filesAggregate = self::collectFiles(
349
            $raw,
350
            $basePath,
351
            $mainScriptPath,
352
            $blacklistFilter,
353
            $excludedPaths,
354
            $alwaysExcludedPaths,
355
            $devPackages,
356
            $composerFiles,
357
            $autodiscoverFiles,
358
            $forceFilesAutodiscovery,
359
            $dumpAutoload,
360
            $logger
361
        );
362
        $binaryFilesAggregate = self::collectBinaryFiles(
363
            $raw,
364
            $basePath,
365
            $blacklistFilter,
366
            $excludedPaths,
367
            $alwaysExcludedPaths,
368
            $devPackages,
369
            $logger
370
        );
371
372
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
373
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
374
375
        $fileMode = self::retrieveFileMode($raw, $logger);
376
377
        $map = self::retrieveMap($raw, $logger);
378
        $fileMapper = new MapFile($basePath, $map);
379
380
        $metadata = self::retrieveMetadata($raw, $logger);
381
382
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
383
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
384
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
385
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
386
387
        $replacements = self::retrieveReplacements($raw, $file, $logger);
388
389
        return new self(
390
            $file,
391
            $alias,
392
            $basePath,
393
            $composerFiles->getComposerJson(),
394
            $composerFiles->getComposerLock(),
395
            $filesAggregate,
396
            $binaryFilesAggregate,
397
            $autodiscoverFiles || $forceFilesAutodiscovery,
398
            $dumpAutoload,
399
            $excludeComposerFiles,
400
            $excludeDevPackages,
401
            $compactors,
402
            $compressionAlgorithm,
403
            $fileMode,
404
            $mainScriptPath,
405
            $mainScriptContents,
406
            $fileMapper,
407
            $metadata,
408
            $tmpOutputPath,
409
            $outputPath,
410
            $privateKeyPassphrase,
411
            $privateKeyPath,
412
            $promptForPrivateKey,
413
            $replacements,
414
            $shebang,
415
            $signingAlgorithm,
416
            $stubBannerContents,
417
            $stubBannerPath,
418
            $stubPath,
419
            $isInterceptsFileFunctions,
420
            $isStubGenerated,
421
            $checkRequirements,
422
            $logger->getWarnings(),
423
            $logger->getRecommendations()
424
        );
425
    }
426
427
    /**
428
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
429
     *                                            path relative to it (the base path)
430
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
431
     *                                            string and the second element its decoded contents as an
432
     *                                            associative array.
433
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
434
     *                                            string and the second element its decoded contents as an
435
     *                                            associative array.
436
     * @param SplFileInfo[] $files                List of files
437
     * @param SplFileInfo[] $binaryFiles          List of binary files
438
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
439
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
440
     *                                            installed.json should be removed from the PHAR
441
     * @param Compactor[]   $compactors           List of file contents compactors
442
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
443
     * @param null|int      $fileMode             File mode in octal form
444
     * @param string        $mainScriptPath       The main script file path
445
     * @param string        $mainScriptContents   The processed content of the main script file
446
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
447
     * @param mixed         $metadata             The PHAR Metadata
448
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
449
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
450
     * @param null|string   $shebang              The shebang line
451
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
452
     * @param null|string   $stubBannerContents   The stub banner comment
453
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
454
     * @param null|string   $stubPath             The PHAR stub file path
455
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
456
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
457
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
458
     *                                            running
459
     * @param string[]      $warnings
460
     * @param string[]      $recommendations
461
     */
462
    private function __construct(
463
        ?string $file,
464
        string $alias,
465
        string $basePath,
466
        ComposerFile $composerJson,
467
        ComposerFile $composerLock,
468
        array $files,
469
        array $binaryFiles,
470
        bool $autodiscoveredFiles,
471
        bool $dumpAutoload,
472
        bool $excludeComposerFiles,
473
        bool $excludeDevPackages,
474
        array $compactors,
475
        ?int $compressionAlgorithm,
476
        ?int $fileMode,
477
        ?string $mainScriptPath,
478
        ?string $mainScriptContents,
479
        MapFile $fileMapper,
480
        $metadata,
481
        string $tmpOutputPath,
482
        string $outputPath,
483
        ?string $privateKeyPassphrase,
484
        ?string $privateKeyPath,
485
        bool $promptForPrivateKey,
486
        array $replacements,
487
        ?string $shebang,
488
        int $signingAlgorithm,
489
        ?string $stubBannerContents,
490
        ?string $stubBannerPath,
491
        ?string $stubPath,
492
        bool $isInterceptFileFuncs,
493
        bool $isStubGenerated,
494
        bool $checkRequirements,
495
        array $warnings,
496
        array $recommendations
497
    ) {
498
        Assertion::nullOrInArray(
499
            $compressionAlgorithm,
500
            get_phar_compression_algorithms(),
501
            sprintf(
502
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
503
                implode('", "', array_keys(get_phar_compression_algorithms()))
504
            )
505
        );
506
507
        if (null === $mainScriptPath) {
508
            Assertion::null($mainScriptContents);
509
        } else {
510
            Assertion::notNull($mainScriptContents);
511
        }
512
513
        $this->file = $file;
514
        $this->alias = $alias;
515
        $this->basePath = $basePath;
516
        $this->composerJson = $composerJson;
517
        $this->composerLock = $composerLock;
518
        $this->files = $files;
519
        $this->binaryFiles = $binaryFiles;
520
        $this->autodiscoveredFiles = $autodiscoveredFiles;
521
        $this->dumpAutoload = $dumpAutoload;
522
        $this->excludeComposerFiles = $excludeComposerFiles;
523
        $this->excludeDevFiles = $excludeDevPackages;
524
        $this->compactors = $compactors;
525
        $this->compressionAlgorithm = $compressionAlgorithm;
526
        $this->fileMode = $fileMode;
527
        $this->mainScriptPath = $mainScriptPath;
528
        $this->mainScriptContents = $mainScriptContents;
529
        $this->fileMapper = $fileMapper;
530
        $this->metadata = $metadata;
531
        $this->tmpOutputPath = $tmpOutputPath;
532
        $this->outputPath = $outputPath;
533
        $this->privateKeyPassphrase = $privateKeyPassphrase;
534
        $this->privateKeyPath = $privateKeyPath;
535
        $this->promptForPrivateKey = $promptForPrivateKey;
536
        $this->processedReplacements = $replacements;
537
        $this->shebang = $shebang;
538
        $this->signingAlgorithm = $signingAlgorithm;
539
        $this->stubBannerContents = $stubBannerContents;
540
        $this->stubBannerPath = $stubBannerPath;
541
        $this->stubPath = $stubPath;
542
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
543
        $this->isStubGenerated = $isStubGenerated;
544
        $this->checkRequirements = $checkRequirements;
545
        $this->warnings = $warnings;
546
        $this->recommendations = $recommendations;
547
    }
548
549
    public function export(): string
550
    {
551
        $exportedConfig = clone $this;
552
553
        $basePath = $exportedConfig->basePath;
554
555
        /**
556
         * @param null|SplFileInfo|string $path
557
         */
558
        $normalizePath = static function ($path) use ($basePath): ?string {
559
            if (null === $path) {
560
                return null;
561
            }
562
563
            if ($path instanceof SplFileInfo) {
564
                $path = $path->getPathname();
565
            }
566
567
            return make_path_relative($path, $basePath);
568
        };
569
570
        $normalizeProperty = static function (&$property) use ($normalizePath): void {
571
            $property = $normalizePath($property);
572
        };
573
574
        $normalizeFiles = static function (&$files) use ($normalizePath): void {
575
            $files = array_map($normalizePath, $files);
576
            sort($files, SORT_STRING);
577
        };
578
579
        $normalizeFiles($exportedConfig->files);
580
        $normalizeFiles($exportedConfig->binaryFiles);
581
582
        $exportedConfig->composerJson = new ComposerFile(
583
            $normalizePath($exportedConfig->composerJson->getPath()),
584
            $exportedConfig->composerJson->getDecodedContents()
585
        );
586
        $exportedConfig->composerLock = new ComposerFile(
587
            $normalizePath($exportedConfig->composerLock->getPath()),
588
            $exportedConfig->composerLock->getDecodedContents()
589
        );
590
591
        $normalizeProperty($exportedConfig->file);
592
        $normalizeProperty($exportedConfig->mainScriptPath);
593
        $normalizeProperty($exportedConfig->tmpOutputPath);
594
        $normalizeProperty($exportedConfig->outputPath);
595
        $normalizeProperty($exportedConfig->privateKeyPath);
596
        $normalizeProperty($exportedConfig->stubBannerPath);
597
        $normalizeProperty($exportedConfig->stubPath);
598
599
        $exportedConfig->compressionAlgorithm = array_flip(get_phar_compression_algorithms())[$exportedConfig->compressionAlgorithm ?? Phar::NONE];
600
        $exportedConfig->signingAlgorithm = array_flip(get_phar_signing_algorithms())[$exportedConfig->signingAlgorithm];
601
        $exportedConfig->compactors = array_map('get_class', $exportedConfig->compactors);
602
        $exportedConfig->fileMode = '0'.decoct($exportedConfig->fileMode);
603
604
        $cloner = new VarCloner();
605
        $cloner->setMaxItems(-1);
606
        $cloner->setMaxString(-1);
607
608
        $splInfoCaster = static function (SplFileInfo $fileInfo) use ($normalizePath): array {
609
            return [$normalizePath($fileInfo)];
610
        };
611
612
        $cloner->addCasters([
613
            SplFileInfo::class => $splInfoCaster,
614
            SymfonySplFileInfo::class => $splInfoCaster,
615
        ]);
616
617
        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...
618
            $cloner->cloneVar($exportedConfig),
619
            true
620
        );
621
    }
622
623
    public function getConfigurationFile(): ?string
624
    {
625
        return $this->file;
626
    }
627
628
    public function getAlias(): string
629
    {
630
        return $this->alias;
631
    }
632
633
    public function getBasePath(): string
634
    {
635
        return $this->basePath;
636
    }
637
638
    public function getComposerJson(): ?string
639
    {
640
        return $this->composerJson->getPath();
641
    }
642
643
    public function getDecodedComposerJsonContents(): ?array
644
    {
645
        return null === $this->composerJson->getPath() ? null : $this->composerJson->getDecodedContents();
646
    }
647
648
    public function getComposerLock(): ?string
649
    {
650
        return $this->composerLock->getPath();
651
    }
652
653
    public function getDecodedComposerLockContents(): ?array
654
    {
655
        return null === $this->composerLock->getPath() ? null : $this->composerLock->getDecodedContents();
656
    }
657
658
    /**
659
     * @return SplFileInfo[]
660
     */
661
    public function getFiles(): array
662
    {
663
        return $this->files;
664
    }
665
666
    /**
667
     * @return SplFileInfo[]
668
     */
669
    public function getBinaryFiles(): array
670
    {
671
        return $this->binaryFiles;
672
    }
673
674
    public function hasAutodiscoveredFiles(): bool
675
    {
676
        return $this->autodiscoveredFiles;
677
    }
678
679
    public function dumpAutoload(): bool
680
    {
681
        return $this->dumpAutoload;
682
    }
683
684
    public function excludeComposerFiles(): bool
685
    {
686
        return $this->excludeComposerFiles;
687
    }
688
689
    public function excludeDevFiles(): bool
690
    {
691
        return $this->excludeDevFiles;
692
    }
693
694
    /**
695
     * @return Compactor[] the list of compactors
696
     */
697
    public function getCompactors(): array
698
    {
699
        return $this->compactors;
700
    }
701
702
    public function getCompressionAlgorithm(): ?int
703
    {
704
        return $this->compressionAlgorithm;
705
    }
706
707
    public function getFileMode(): ?int
708
    {
709
        return $this->fileMode;
710
    }
711
712
    public function hasMainScript(): bool
713
    {
714
        return null !== $this->mainScriptPath;
715
    }
716
717
    public function getMainScriptPath(): string
718
    {
719
        Assertion::notNull(
720
            $this->mainScriptPath,
721
            'Cannot retrieve the main script path: no main script configured.'
722
        );
723
724
        return $this->mainScriptPath;
725
    }
726
727
    public function getMainScriptContents(): string
728
    {
729
        Assertion::notNull(
730
            $this->mainScriptPath,
731
            'Cannot retrieve the main script contents: no main script configured.'
732
        );
733
734
        return $this->mainScriptContents;
735
    }
736
737
    public function checkRequirements(): bool
738
    {
739
        return $this->checkRequirements;
740
    }
741
742
    public function getTmpOutputPath(): string
743
    {
744
        return $this->tmpOutputPath;
745
    }
746
747
    public function getOutputPath(): string
748
    {
749
        return $this->outputPath;
750
    }
751
752
    public function getFileMapper(): MapFile
753
    {
754
        return $this->fileMapper;
755
    }
756
757
    /**
758
     * @return mixed
759
     */
760
    public function getMetadata()
761
    {
762
        return $this->metadata;
763
    }
764
765
    public function getPrivateKeyPassphrase(): ?string
766
    {
767
        return $this->privateKeyPassphrase;
768
    }
769
770
    public function getPrivateKeyPath(): ?string
771
    {
772
        return $this->privateKeyPath;
773
    }
774
775
    /**
776
     * @deprecated Use promptForPrivateKey() instead
777
     */
778
    public function isPrivateKeyPrompt(): bool
779
    {
780
        return $this->promptForPrivateKey;
781
    }
782
783
    public function promptForPrivateKey(): bool
784
    {
785
        return $this->promptForPrivateKey;
786
    }
787
788
    /**
789
     * @return scalar[]
790
     */
791
    public function getReplacements(): array
792
    {
793
        return $this->processedReplacements;
794
    }
795
796
    public function getShebang(): ?string
797
    {
798
        return $this->shebang;
799
    }
800
801
    public function getSigningAlgorithm(): int
802
    {
803
        return $this->signingAlgorithm;
804
    }
805
806
    public function getStubBannerContents(): ?string
807
    {
808
        return $this->stubBannerContents;
809
    }
810
811
    public function getStubBannerPath(): ?string
812
    {
813
        return $this->stubBannerPath;
814
    }
815
816
    public function getStubPath(): ?string
817
    {
818
        return $this->stubPath;
819
    }
820
821
    public function isInterceptFileFuncs(): bool
822
    {
823
        return $this->isInterceptFileFuncs;
824
    }
825
826
    public function isStubGenerated(): bool
827
    {
828
        return $this->isStubGenerated;
829
    }
830
831
    /**
832
     * @return string[]
833
     */
834
    public function getWarnings(): array
835
    {
836
        return $this->warnings;
837
    }
838
839
    /**
840
     * @return string[]
841
     */
842
    public function getRecommendations(): array
843
    {
844
        return $this->recommendations;
845
    }
846
847
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
848
    {
849
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
850
851
        if (false === isset($raw->{self::ALIAS_KEY})) {
852
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
853
        }
854
855
        $alias = trim($raw->{self::ALIAS_KEY});
856
857
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
858
859
        if ($userStubUsed) {
860
            $logger->addWarning(
861
                sprintf(
862
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
863
                    self::ALIAS_KEY
864
                )
865
            );
866
        }
867
868
        return $alias;
869
    }
870
871
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
872
    {
873
        if (null === $file) {
874
            return getcwd();
875
        }
876
877
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
878
            return realpath(dirname($file));
879
        }
880
881
        $basePath = trim($raw->{self::BASE_PATH_KEY});
882
883
        Assertion::directory(
884
            $basePath,
885
            'The base path "%s" is not a directory or does not exist.'
886
        );
887
888
        $basePath = realpath($basePath);
889
        $defaultPath = realpath(dirname($file));
890
891
        if ($basePath === $defaultPath) {
892
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
893
        }
894
895
        return $basePath;
896
    }
897
898
    /**
899
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
900
     */
901
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
902
    {
903
        if (null === $file) {
904
            return true;
905
        }
906
907
        $associativeRaw = (array) $raw;
908
909
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
910
    }
911
912
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
913
    {
914
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
915
916
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
917
    }
918
919
    private static function retrieveBlacklistFilter(
920
        stdClass $raw,
921
        string $basePath,
922
        ConfigurationLogger $logger,
923
        ?string ...$excludedPaths
924
    ): array {
925
        $blacklist = array_flip(
926
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
927
        );
928
929
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
930
            if ($file->isLink()) {
931
                return false;
932
            }
933
934
            if (false === $file->getRealPath()) {
935
                return false;
936
            }
937
938
            if (array_key_exists($file->getRealPath(), $blacklist)) {
939
                return false;
940
            }
941
942
            return null;
943
        };
944
945
        return [array_keys($blacklist), $blacklistFilter];
946
    }
947
948
    /**
949
     * @param null[]|string[] $excludedPaths
950
     *
951
     * @return string[]
952
     */
953
    private static function retrieveBlacklist(
954
        stdClass $raw,
955
        string $basePath,
956
        ConfigurationLogger $logger,
957
        ?string ...$excludedPaths
958
    ): array {
959
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
960
961
        $normalizedBlacklist = array_map(
962
            static function (string $excludedPath) use ($basePath): string {
963
                return self::normalizePath($excludedPath, $basePath);
964
            },
965
            array_filter($excludedPaths)
966
        );
967
968
        /** @var string[] $blacklist */
969
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
970
971
        foreach ($blacklist as $file) {
972
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
973
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
974
        }
975
976
        return array_unique($normalizedBlacklist);
977
    }
978
979
    /**
980
     * @param string[] $excludedPaths
981
     * @param string[] $alwaysExcludedPaths
982
     * @param string[] $devPackages
983
     *
984
     * @return SplFileInfo[]
985
     */
986
    private static function collectFiles(
987
        stdClass $raw,
988
        string $basePath,
989
        ?string $mainScriptPath,
990
        Closure $blacklistFilter,
991
        array $excludedPaths,
992
        array $alwaysExcludedPaths,
993
        array $devPackages,
994
        ComposerFiles $composerFiles,
995
        bool $autodiscoverFiles,
996
        bool $forceFilesAutodiscovery,
997
        bool $dumpAutoload,
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
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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