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

Configuration::retrieveGitTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use function array_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
        if ($dumpAutoload && null !== $composerFiles->getInstalledJson()->getPath() && null === $composerFiles->getComposerLock()->getPath()) {
269
            $logger->addWarning(
270
                'A vendor/composer/installed.json file has been found but its related file composer.lock could not. '
271
                .'This is likely due to the file having been removed despite being necessary. This will not break the '
272
                .'build but the dump-autoload had to be disabled.'
273
            );
274
        }
275
276
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
277
278
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles->getComposerJson()->getDecodedContents(), $logger);
279
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
280
281
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
282
283
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
284
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
285
286
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
287
288
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
289
290
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
291
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
292
293
        if (null !== $stubBannerPath) {
294
            $stubBannerContents = file_contents($stubBannerPath);
295
        }
296
297
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
298
299
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
300
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
301
        }
302
303
        $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger);
304
305
        $checkRequirements = self::retrieveCheckRequirements(
306
            $raw,
307
            null !== $composerFiles->getComposerJson()->getPath(),
308
            null !== $composerFiles->getComposerLock()->getPath(),
309
            false === $isStubGenerated && null === $stubPath,
310
            $logger
311
        );
312
313
        $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger);
314
315
        $devPackages = ComposerConfiguration::retrieveDevPackages(
316
            $basePath,
317
            $composerFiles->getComposerJson()->getDecodedContents(),
318
            $composerFiles->getComposerLock()->getDecodedContents(),
319
            $excludeDevPackages
320
        );
321
322
        /**
323
         * @var string[]
324
         * @var Closure  $blacklistFilter
325
         */
326
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
327
            $raw,
328
            $basePath,
329
            $logger,
330
            $tmpOutputPath,
331
            $outputPath,
332
            $mainScriptPath
333
        );
334
        // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist.
335
        // If you consider the following:
336
        //
337
        // {
338
        //   "files": ["file1"],
339
        //   "blacklist": ["file1"],
340
        // }
341
        //
342
        // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders
343
        // and directories but the user should always have the possibility to force his way to include a file.
344
        //
345
        // The exception however, is for the following which is essential for the good functioning of Box
346
        $alwaysExcludedPaths = array_map(
347
            static function (string $excludedPath) use ($basePath): string {
348
                return self::normalizePath($excludedPath, $basePath);
349
            },
350
            array_filter([$tmpOutputPath, $outputPath, $mainScriptPath])
351
        );
352
353
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
354
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
355
356
        $filesAggregate = self::collectFiles(
357
            $raw,
358
            $basePath,
359
            $mainScriptPath,
360
            $blacklistFilter,
361
            $excludedPaths,
362
            $alwaysExcludedPaths,
363
            $devPackages,
364
            $composerFiles,
365
            $autodiscoverFiles,
366
            $forceFilesAutodiscovery,
367
            $dumpAutoload,
368
            $logger
369
        );
370
        $binaryFilesAggregate = self::collectBinaryFiles(
371
            $raw,
372
            $basePath,
373
            $blacklistFilter,
374
            $excludedPaths,
375
            $alwaysExcludedPaths,
376
            $devPackages,
377
            $logger
378
        );
379
380
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
381
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
382
383
        $fileMode = self::retrieveFileMode($raw, $logger);
384
385
        $map = self::retrieveMap($raw, $logger);
386
        $fileMapper = new MapFile($basePath, $map);
387
388
        $metadata = self::retrieveMetadata($raw, $logger);
389
390
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
391
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
392
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
393
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
394
395
        $replacements = self::retrieveReplacements($raw, $file, $logger);
396
397
        return new self(
398
            $file,
399
            $alias,
400
            $basePath,
401
            $composerFiles->getComposerJson(),
402
            $composerFiles->getComposerLock(),
403
            $filesAggregate,
404
            $binaryFilesAggregate,
405
            $autodiscoverFiles || $forceFilesAutodiscovery,
406
            $dumpAutoload,
407
            $excludeComposerFiles,
408
            $excludeDevPackages,
409
            $compactors,
410
            $compressionAlgorithm,
411
            $fileMode,
412
            $mainScriptPath,
413
            $mainScriptContents,
414
            $fileMapper,
415
            $metadata,
416
            $tmpOutputPath,
417
            $outputPath,
418
            $privateKeyPassphrase,
419
            $privateKeyPath,
420
            $promptForPrivateKey,
421
            $replacements,
422
            $shebang,
423
            $signingAlgorithm,
424
            $stubBannerContents,
425
            $stubBannerPath,
426
            $stubPath,
427
            $isInterceptsFileFunctions,
428
            $isStubGenerated,
429
            $checkRequirements,
430
            $logger->getWarnings(),
431
            $logger->getRecommendations()
432
        );
433
    }
434
435
    /**
436
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
437
     *                                            path relative to it (the base path)
438
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
439
     *                                            string and the second element its decoded contents as an
440
     *                                            associative array.
441
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
442
     *                                            string and the second element its decoded contents as an
443
     *                                            associative array.
444
     * @param SplFileInfo[] $files                List of files
445
     * @param SplFileInfo[] $binaryFiles          List of binary files
446
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
447
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
448
     *                                            installed.json should be removed from the PHAR
449
     * @param Compactor[]   $compactors           List of file contents compactors
450
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
451
     * @param null|int      $fileMode             File mode in octal form
452
     * @param string        $mainScriptPath       The main script file path
453
     * @param string        $mainScriptContents   The processed content of the main script file
454
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
455
     * @param mixed         $metadata             The PHAR Metadata
456
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
457
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
458
     * @param null|string   $shebang              The shebang line
459
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
460
     * @param null|string   $stubBannerContents   The stub banner comment
461
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
462
     * @param null|string   $stubPath             The PHAR stub file path
463
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
464
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
465
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
466
     *                                            running
467
     * @param string[]      $warnings
468
     * @param string[]      $recommendations
469
     */
470
    private function __construct(
471
        ?string $file,
472
        string $alias,
473
        string $basePath,
474
        ComposerFile $composerJson,
475
        ComposerFile $composerLock,
476
        array $files,
477
        array $binaryFiles,
478
        bool $autodiscoveredFiles,
479
        bool $dumpAutoload,
480
        bool $excludeComposerFiles,
481
        bool $excludeDevPackages,
482
        array $compactors,
483
        ?int $compressionAlgorithm,
484
        ?int $fileMode,
485
        ?string $mainScriptPath,
486
        ?string $mainScriptContents,
487
        MapFile $fileMapper,
488
        $metadata,
489
        string $tmpOutputPath,
490
        string $outputPath,
491
        ?string $privateKeyPassphrase,
492
        ?string $privateKeyPath,
493
        bool $promptForPrivateKey,
494
        array $replacements,
495
        ?string $shebang,
496
        int $signingAlgorithm,
497
        ?string $stubBannerContents,
498
        ?string $stubBannerPath,
499
        ?string $stubPath,
500
        bool $isInterceptFileFuncs,
501
        bool $isStubGenerated,
502
        bool $checkRequirements,
503
        array $warnings,
504
        array $recommendations
505
    ) {
506
        Assertion::nullOrInArray(
507
            $compressionAlgorithm,
508
            get_phar_compression_algorithms(),
509
            sprintf(
510
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
511
                implode('", "', array_keys(get_phar_compression_algorithms()))
512
            )
513
        );
514
515
        if (null === $mainScriptPath) {
516
            Assertion::null($mainScriptContents);
517
        } else {
518
            Assertion::notNull($mainScriptContents);
519
        }
520
521
        $this->file = $file;
522
        $this->alias = $alias;
523
        $this->basePath = $basePath;
524
        $this->composerJson = $composerJson;
525
        $this->composerLock = $composerLock;
526
        $this->files = $files;
527
        $this->binaryFiles = $binaryFiles;
528
        $this->autodiscoveredFiles = $autodiscoveredFiles;
529
        $this->dumpAutoload = $dumpAutoload;
530
        $this->excludeComposerFiles = $excludeComposerFiles;
531
        $this->excludeDevFiles = $excludeDevPackages;
532
        $this->compactors = $compactors;
533
        $this->compressionAlgorithm = $compressionAlgorithm;
534
        $this->fileMode = $fileMode;
535
        $this->mainScriptPath = $mainScriptPath;
536
        $this->mainScriptContents = $mainScriptContents;
537
        $this->fileMapper = $fileMapper;
538
        $this->metadata = $metadata;
539
        $this->tmpOutputPath = $tmpOutputPath;
540
        $this->outputPath = $outputPath;
541
        $this->privateKeyPassphrase = $privateKeyPassphrase;
542
        $this->privateKeyPath = $privateKeyPath;
543
        $this->promptForPrivateKey = $promptForPrivateKey;
544
        $this->processedReplacements = $replacements;
545
        $this->shebang = $shebang;
546
        $this->signingAlgorithm = $signingAlgorithm;
547
        $this->stubBannerContents = $stubBannerContents;
548
        $this->stubBannerPath = $stubBannerPath;
549
        $this->stubPath = $stubPath;
550
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
551
        $this->isStubGenerated = $isStubGenerated;
552
        $this->checkRequirements = $checkRequirements;
553
        $this->warnings = $warnings;
554
        $this->recommendations = $recommendations;
555
    }
556
557
    public function export(): string
558
    {
559
        $exportedConfig = clone $this;
560
561
        $basePath = $exportedConfig->basePath;
562
563
        /**
564
         * @param null|SplFileInfo|string $path
565
         */
566
        $normalizePath = static function ($path) use ($basePath): ?string {
567
            if (null === $path) {
568
                return null;
569
            }
570
571
            if ($path instanceof SplFileInfo) {
572
                $path = $path->getPathname();
573
            }
574
575
            return make_path_relative($path, $basePath);
576
        };
577
578
        $normalizeProperty = static function (&$property) use ($normalizePath): void {
579
            $property = $normalizePath($property);
580
        };
581
582
        $normalizeFiles = static function (&$files) use ($normalizePath): void {
583
            $files = array_map($normalizePath, $files);
584
            sort($files, SORT_STRING);
585
        };
586
587
        $normalizeFiles($exportedConfig->files);
588
        $normalizeFiles($exportedConfig->binaryFiles);
589
590
        $exportedConfig->composerJson = new ComposerFile(
591
            $normalizePath($exportedConfig->composerJson->getPath()),
592
            $exportedConfig->composerJson->getDecodedContents()
593
        );
594
        $exportedConfig->composerLock = new ComposerFile(
595
            $normalizePath($exportedConfig->composerLock->getPath()),
596
            $exportedConfig->composerLock->getDecodedContents()
597
        );
598
599
        $normalizeProperty($exportedConfig->file);
600
        $normalizeProperty($exportedConfig->mainScriptPath);
601
        $normalizeProperty($exportedConfig->tmpOutputPath);
602
        $normalizeProperty($exportedConfig->outputPath);
603
        $normalizeProperty($exportedConfig->privateKeyPath);
604
        $normalizeProperty($exportedConfig->stubBannerPath);
605
        $normalizeProperty($exportedConfig->stubPath);
606
607
        $exportedConfig->compressionAlgorithm = array_flip(get_phar_compression_algorithms())[$exportedConfig->compressionAlgorithm ?? Phar::NONE];
608
        $exportedConfig->signingAlgorithm = array_flip(get_phar_signing_algorithms())[$exportedConfig->signingAlgorithm];
609
        $exportedConfig->compactors = array_map('get_class', $exportedConfig->compactors);
610
        $exportedConfig->fileMode = '0'.decoct($exportedConfig->fileMode);
611
612
        $cloner = new VarCloner();
613
        $cloner->setMaxItems(-1);
614
        $cloner->setMaxString(-1);
615
616
        $splInfoCaster = static function (SplFileInfo $fileInfo) use ($normalizePath): array {
617
            return [$normalizePath($fileInfo)];
618
        };
619
620
        $cloner->addCasters([
621
            SplFileInfo::class => $splInfoCaster,
622
            SymfonySplFileInfo::class => $splInfoCaster,
623
        ]);
624
625
        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...
626
            $cloner->cloneVar($exportedConfig),
627
            true
628
        );
629
    }
630
631
    public function getConfigurationFile(): ?string
632
    {
633
        return $this->file;
634
    }
635
636
    public function getAlias(): string
637
    {
638
        return $this->alias;
639
    }
640
641
    public function getBasePath(): string
642
    {
643
        return $this->basePath;
644
    }
645
646
    public function getComposerJson(): ?string
647
    {
648
        return $this->composerJson->getPath();
649
    }
650
651
    public function getDecodedComposerJsonContents(): ?array
652
    {
653
        return null === $this->composerJson->getPath() ? null : $this->composerJson->getDecodedContents();
654
    }
655
656
    public function getComposerLock(): ?string
657
    {
658
        return $this->composerLock->getPath();
659
    }
660
661
    public function getDecodedComposerLockContents(): ?array
662
    {
663
        return null === $this->composerLock->getPath() ? null : $this->composerLock->getDecodedContents();
664
    }
665
666
    /**
667
     * @return SplFileInfo[]
668
     */
669
    public function getFiles(): array
670
    {
671
        return $this->files;
672
    }
673
674
    /**
675
     * @return SplFileInfo[]
676
     */
677
    public function getBinaryFiles(): array
678
    {
679
        return $this->binaryFiles;
680
    }
681
682
    public function hasAutodiscoveredFiles(): bool
683
    {
684
        return $this->autodiscoveredFiles;
685
    }
686
687
    public function dumpAutoload(): bool
688
    {
689
        return $this->dumpAutoload;
690
    }
691
692
    public function excludeComposerFiles(): bool
693
    {
694
        return $this->excludeComposerFiles;
695
    }
696
697
    public function excludeDevFiles(): bool
698
    {
699
        return $this->excludeDevFiles;
700
    }
701
702
    /**
703
     * @return Compactor[] the list of compactors
704
     */
705
    public function getCompactors(): array
706
    {
707
        return $this->compactors;
708
    }
709
710
    public function getCompressionAlgorithm(): ?int
711
    {
712
        return $this->compressionAlgorithm;
713
    }
714
715
    public function getFileMode(): ?int
716
    {
717
        return $this->fileMode;
718
    }
719
720
    public function hasMainScript(): bool
721
    {
722
        return null !== $this->mainScriptPath;
723
    }
724
725
    public function getMainScriptPath(): string
726
    {
727
        Assertion::notNull(
728
            $this->mainScriptPath,
729
            'Cannot retrieve the main script path: no main script configured.'
730
        );
731
732
        return $this->mainScriptPath;
733
    }
734
735
    public function getMainScriptContents(): string
736
    {
737
        Assertion::notNull(
738
            $this->mainScriptPath,
739
            'Cannot retrieve the main script contents: no main script configured.'
740
        );
741
742
        return $this->mainScriptContents;
743
    }
744
745
    public function checkRequirements(): bool
746
    {
747
        return $this->checkRequirements;
748
    }
749
750
    public function getTmpOutputPath(): string
751
    {
752
        return $this->tmpOutputPath;
753
    }
754
755
    public function getOutputPath(): string
756
    {
757
        return $this->outputPath;
758
    }
759
760
    public function getFileMapper(): MapFile
761
    {
762
        return $this->fileMapper;
763
    }
764
765
    /**
766
     * @return mixed
767
     */
768
    public function getMetadata()
769
    {
770
        return $this->metadata;
771
    }
772
773
    public function getPrivateKeyPassphrase(): ?string
774
    {
775
        return $this->privateKeyPassphrase;
776
    }
777
778
    public function getPrivateKeyPath(): ?string
779
    {
780
        return $this->privateKeyPath;
781
    }
782
783
    /**
784
     * @deprecated Use promptForPrivateKey() instead
785
     */
786
    public function isPrivateKeyPrompt(): bool
787
    {
788
        return $this->promptForPrivateKey;
789
    }
790
791
    public function promptForPrivateKey(): bool
792
    {
793
        return $this->promptForPrivateKey;
794
    }
795
796
    /**
797
     * @return scalar[]
798
     */
799
    public function getReplacements(): array
800
    {
801
        return $this->processedReplacements;
802
    }
803
804
    public function getShebang(): ?string
805
    {
806
        return $this->shebang;
807
    }
808
809
    public function getSigningAlgorithm(): int
810
    {
811
        return $this->signingAlgorithm;
812
    }
813
814
    public function getStubBannerContents(): ?string
815
    {
816
        return $this->stubBannerContents;
817
    }
818
819
    public function getStubBannerPath(): ?string
820
    {
821
        return $this->stubBannerPath;
822
    }
823
824
    public function getStubPath(): ?string
825
    {
826
        return $this->stubPath;
827
    }
828
829
    public function isInterceptFileFuncs(): bool
830
    {
831
        return $this->isInterceptFileFuncs;
832
    }
833
834
    public function isStubGenerated(): bool
835
    {
836
        return $this->isStubGenerated;
837
    }
838
839
    /**
840
     * @return string[]
841
     */
842
    public function getWarnings(): array
843
    {
844
        return $this->warnings;
845
    }
846
847
    /**
848
     * @return string[]
849
     */
850
    public function getRecommendations(): array
851
    {
852
        return $this->recommendations;
853
    }
854
855
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
856
    {
857
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
858
859
        if (false === isset($raw->{self::ALIAS_KEY})) {
860
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
861
        }
862
863
        $alias = trim($raw->{self::ALIAS_KEY});
864
865
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
866
867
        if ($userStubUsed) {
868
            $logger->addWarning(
869
                sprintf(
870
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
871
                    self::ALIAS_KEY
872
                )
873
            );
874
        }
875
876
        return $alias;
877
    }
878
879
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
880
    {
881
        if (null === $file) {
882
            return getcwd();
883
        }
884
885
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
886
            return realpath(dirname($file));
887
        }
888
889
        $basePath = trim($raw->{self::BASE_PATH_KEY});
890
891
        Assertion::directory(
892
            $basePath,
893
            'The base path "%s" is not a directory or does not exist.'
894
        );
895
896
        $basePath = realpath($basePath);
897
        $defaultPath = realpath(dirname($file));
898
899
        if ($basePath === $defaultPath) {
900
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
901
        }
902
903
        return $basePath;
904
    }
905
906
    /**
907
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
908
     */
909
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
910
    {
911
        if (null === $file) {
912
            return true;
913
        }
914
915
        $associativeRaw = (array) $raw;
916
917
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
918
    }
919
920
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
921
    {
922
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
923
924
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
925
    }
926
927
    private static function retrieveBlacklistFilter(
928
        stdClass $raw,
929
        string $basePath,
930
        ConfigurationLogger $logger,
931
        ?string ...$excludedPaths
932
    ): array {
933
        $blacklist = array_flip(
934
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
935
        );
936
937
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
938
            if ($file->isLink()) {
939
                return false;
940
            }
941
942
            if (false === $file->getRealPath()) {
943
                return false;
944
            }
945
946
            if (array_key_exists($file->getRealPath(), $blacklist)) {
947
                return false;
948
            }
949
950
            return null;
951
        };
952
953
        return [array_keys($blacklist), $blacklistFilter];
954
    }
955
956
    /**
957
     * @param null[]|string[] $excludedPaths
958
     *
959
     * @return string[]
960
     */
961
    private static function retrieveBlacklist(
962
        stdClass $raw,
963
        string $basePath,
964
        ConfigurationLogger $logger,
965
        ?string ...$excludedPaths
966
    ): array {
967
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
968
969
        $normalizedBlacklist = array_map(
970
            static function (string $excludedPath) use ($basePath): string {
971
                return self::normalizePath($excludedPath, $basePath);
972
            },
973
            array_filter($excludedPaths)
974
        );
975
976
        /** @var string[] $blacklist */
977
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
978
979
        foreach ($blacklist as $file) {
980
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
981
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
982
        }
983
984
        return array_unique($normalizedBlacklist);
985
    }
986
987
    /**
988
     * @param string[] $excludedPaths
989
     * @param string[] $alwaysExcludedPaths
990
     * @param string[] $devPackages
991
     *
992
     * @return SplFileInfo[]
993
     */
994
    private static function collectFiles(
995
        stdClass $raw,
996
        string $basePath,
997
        ?string $mainScriptPath,
998
        Closure $blacklistFilter,
999
        array $excludedPaths,
1000
        array $alwaysExcludedPaths,
1001
        array $devPackages,
1002
        ComposerFiles $composerFiles,
1003
        bool $autodiscoverFiles,
1004
        bool $forceFilesAutodiscovery,
1005
        bool $dumpAutoload,
1006
        ConfigurationLogger $logger
1007
    ): array {
1008
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)];
1009
1010
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
1011
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
1012
                $basePath,
1013
                $composerFiles->getComposerJson()->getDecodedContents(),
1014
                $devPackages,
1015
                $composerFiles->getPaths(),
1016
                $excludedPaths
1017
            );
1018
1019
            $files[] = self::wrapInSplFileInfo($filesToAppend);
1020
1021
            $files[] = self::retrieveAllFiles(
1022
                $basePath,
1023
                $directories,
1024
                $mainScriptPath,
1025
                $blacklistFilter,
1026
                $excludedPaths,
1027
                $devPackages
1028
            );
1029
        }
1030
1031
        if (false === $autodiscoverFiles) {
1032
            $files[] = self::retrieveDirectories(
1033
                $raw,
1034
                self::DIRECTORIES_KEY,
1035
                $basePath,
1036
                $blacklistFilter,
1037
                $excludedPaths,
1038
                $logger
1039
            );
1040
1041
            $filesFromFinders = self::retrieveFilesFromFinders(
1042
                $raw,
1043
                self::FINDER_KEY,
1044
                $basePath,
1045
                $blacklistFilter,
1046
                $devPackages,
1047
                $logger
1048
            );
1049
1050
            foreach ($filesFromFinders as $filesFromFinder) {
1051
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
1052
                $files[] = $filesFromFinder;
1053
            }
1054
1055
            $files[] = self::wrapInSplFileInfo($composerFiles->getPaths());
1056
        }
1057
1058
        return self::retrieveFilesAggregate(...$files);
1059
    }
1060
1061
    /**
1062
     * @param string[] $excludedPaths
1063
     * @param string[] $alwaysExcludedPaths
1064
     * @param string[] $devPackages
1065
     *
1066
     * @return SplFileInfo[]
1067
     */
1068
    private static function collectBinaryFiles(
1069
        stdClass $raw,
1070
        string $basePath,
1071
        Closure $blacklistFilter,
1072
        array $excludedPaths,
1073
        array $alwaysExcludedPaths,
1074
        array $devPackages,
1075
        ConfigurationLogger $logger
1076
    ): array {
1077
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, ComposerFiles::createEmpty(), $alwaysExcludedPaths, $logger);
1078
1079
        $binaryDirectories = self::retrieveDirectories(
1080
            $raw,
1081
            self::DIRECTORIES_BIN_KEY,
1082
            $basePath,
1083
            $blacklistFilter,
1084
            $excludedPaths,
1085
            $logger
1086
        );
1087
1088
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
1089
            $raw,
1090
            self::FINDER_BIN_KEY,
1091
            $basePath,
1092
            $blacklistFilter,
1093
            $devPackages,
1094
            $logger
1095
        );
1096
1097
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
1098
    }
1099
1100
    /**
1101
     * @param string[] $excludedFiles
1102
     *
1103
     * @return SplFileInfo[]
1104
     */
1105
    private static function retrieveFiles(
1106
        stdClass $raw,
1107
        string $key,
1108
        string $basePath,
1109
        ComposerFiles $composerFiles,
1110
        array $excludedFiles,
1111
        ConfigurationLogger $logger
1112
    ): array {
1113
        self::checkIfDefaultValue($logger, $raw, $key, []);
1114
1115
        $excludedFiles = array_flip($excludedFiles);
1116
        $files = array_filter([
1117
            $composerFiles->getComposerJson()->getPath(),
1118
            $composerFiles->getComposerLock()->getPath(),
1119
        ]);
1120
1121
        if (false === isset($raw->{$key})) {
1122
            return self::wrapInSplFileInfo($files);
1123
        }
1124
1125
        if ([] === (array) $raw->{$key}) {
1126
            return self::wrapInSplFileInfo($files);
1127
        }
1128
1129
        $files = array_merge((array) $raw->{$key}, $files);
1130
1131
        Assertion::allString($files);
1132
1133
        $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo {
1134
            $file = self::normalizePath($file, $basePath);
1135
1136
            Assertion::false(
1137
                is_link($file),
1138
                sprintf(
1139
                    'Cannot add the link "%s": links are not supported.',
1140
                    $file
1141
                )
1142
            );
1143
1144
            Assertion::file(
1145
                $file,
1146
                sprintf(
1147
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1148
                    $key
1149
                )
1150
            );
1151
1152
            return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file);
1153
        };
1154
1155
        return array_filter(array_map($normalizePath, $files));
1156
    }
1157
1158
    /**
1159
     * @param string   $key           Config property name
1160
     * @param string[] $excludedPaths
1161
     *
1162
     * @return iterable|SplFileInfo[]
1163
     */
1164
    private static function retrieveDirectories(
1165
        stdClass $raw,
1166
        string $key,
1167
        string $basePath,
1168
        Closure $blacklistFilter,
1169
        array $excludedPaths,
1170
        ConfigurationLogger $logger
1171
    ): iterable {
1172
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1173
1174
        if ([] !== $directories) {
1175
            $finder = Finder::create()
1176
                ->files()
1177
                ->filter($blacklistFilter)
1178
                ->ignoreVCS(true)
1179
                ->in($directories)
1180
            ;
1181
1182
            foreach ($excludedPaths as $excludedPath) {
1183
                $finder->notPath($excludedPath);
1184
            }
1185
1186
            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...
1187
        }
1188
1189
        return [];
1190
    }
1191
1192
    /**
1193
     * @param string[] $devPackages
1194
     *
1195
     * @return iterable[]|SplFileInfo[][]
1196
     */
1197
    private static function retrieveFilesFromFinders(
1198
        stdClass $raw,
1199
        string $key,
1200
        string $basePath,
1201
        Closure $blacklistFilter,
1202
        array $devPackages,
1203
        ConfigurationLogger $logger
1204
    ): array {
1205
        self::checkIfDefaultValue($logger, $raw, $key, []);
1206
1207
        if (false === isset($raw->{$key})) {
1208
            return [];
1209
        }
1210
1211
        $finder = $raw->{$key};
1212
1213
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1214
    }
1215
1216
    /**
1217
     * @param iterable[]|SplFileInfo[][] $fileIterators
1218
     *
1219
     * @return SplFileInfo[]
1220
     */
1221
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1222
    {
1223
        $files = [];
1224
1225
        foreach ($fileIterators as $fileIterator) {
1226
            foreach ($fileIterator as $file) {
1227
                $files[(string) $file] = $file;
1228
            }
1229
        }
1230
1231
        return array_values($files);
1232
    }
1233
1234
    /**
1235
     * @param string[] $devPackages
1236
     *
1237
     * @return Finder[]|SplFileInfo[][]
1238
     */
1239
    private static function processFinders(
1240
        array $findersConfig,
1241
        string $basePath,
1242
        Closure $blacklistFilter,
1243
        array $devPackages
1244
    ): array {
1245
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1246
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1247
        };
1248
1249
        return array_map($processFinderConfig, $findersConfig);
1250
    }
1251
1252
    /**
1253
     * @param string[] $devPackages
1254
     *
1255
     * @return Finder|SplFileInfo[]
1256
     */
1257
    private static function processFinder(
1258
        stdClass $config,
1259
        string $basePath,
1260
        Closure $blacklistFilter,
1261
        array $devPackages
1262
    ): Finder {
1263
        $finder = Finder::create()
1264
            ->files()
1265
            ->filter($blacklistFilter)
1266
            ->filter(
1267
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1268
                    foreach ($devPackages as $devPackage) {
1269
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1270
                            // File belongs to the dev package
1271
                            return false;
1272
                        }
1273
                    }
1274
1275
                    return true;
1276
                }
1277
            )
1278
            ->ignoreVCS(true)
1279
        ;
1280
1281
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1282
            $normalizedConfig = [];
1283
1284
            foreach ($config as $method => $arguments) {
1285
                $method = trim($method);
1286
                $arguments = (array) $arguments;
1287
1288
                Assertion::methodExists(
1289
                    $method,
1290
                    $finder,
1291
                    'The method "Finder::%s" does not exist.'
1292
                );
1293
1294
                $normalizedConfig[$method] = $arguments;
1295
            }
1296
1297
            krsort($normalizedConfig);
1298
1299
            return $normalizedConfig;
1300
        })((array) $config, $finder);
1301
1302
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1303
            $directory = self::normalizePath($directory, $basePath);
1304
1305
            Assertion::false(
1306
                is_link($directory),
1307
                sprintf(
1308
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1309
                    $directory
1310
                )
1311
            );
1312
1313
            Assertion::directory($directory);
1314
1315
            return $directory;
1316
        };
1317
1318
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1319
            $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

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