Passed
Pull Request — master (#406)
by Théo
02:37
created

Configuration::getComposerJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\Configuration;
16
17
use function array_diff;
18
use function array_filter;
19
use function array_flip;
20
use function array_key_exists;
21
use function array_keys;
22
use function array_map;
23
use function array_merge;
24
use function array_unique;
25
use function array_values;
26
use function array_walk;
27
use Assert\Assertion;
28
use Closure;
29
use function constant;
30
use function current;
31
use DateTimeImmutable;
32
use DateTimeZone;
33
use function defined;
34
use function dirname;
35
use const E_USER_DEPRECATED;
36
use function explode;
37
use function file_exists;
38
use function get_class;
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\Compactor;
64
use KevinGH\Box\Compactor\CompactorProxy;
65
use KevinGH\Box\Compactor\Compactors;
66
use KevinGH\Box\Compactor\Json as JsonCompactor;
67
use KevinGH\Box\Compactor\Php as PhpCompactor;
68
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
69
use KevinGH\Box\Composer\ComposerConfiguration;
70
use KevinGH\Box\Composer\ComposerFile;
71
use KevinGH\Box\Composer\ComposerFiles;
72
use function KevinGH\Box\FileSystem\canonicalize;
73
use function KevinGH\Box\FileSystem\file_contents;
74
use function KevinGH\Box\FileSystem\is_absolute_path;
75
use function KevinGH\Box\FileSystem\longest_common_base_path;
76
use function KevinGH\Box\FileSystem\make_path_absolute;
77
use function KevinGH\Box\FileSystem\make_path_relative;
78
use function KevinGH\Box\get_box_version;
79
use function KevinGH\Box\get_phar_compression_algorithms;
80
use function KevinGH\Box\get_phar_signing_algorithms;
81
use KevinGH\Box\Json\Json;
82
use KevinGH\Box\MapFile;
83
use KevinGH\Box\PhpScoper\SimpleScoper;
84
use function KevinGH\Box\unique_id;
85
use function krsort;
86
use Phar;
87
use function preg_match;
88
use function preg_replace;
89
use function property_exists;
90
use function realpath;
91
use RuntimeException;
92
use Seld\JsonLint\ParsingException;
93
use function sort;
94
use const SORT_STRING;
95
use SplFileInfo;
96
use function sprintf;
97
use stdClass;
98
use function strtoupper;
99
use function substr;
100
use Symfony\Component\Finder\Finder;
101
use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo;
102
use Symfony\Component\Process\Process;
103
use Symfony\Component\VarDumper\Cloner\VarCloner;
104
use Symfony\Component\VarDumper\Dumper\CliDumper;
105
use function trigger_error;
106
use function trim;
107
108
/**
109
 * @private
110
 */
111
final class Configuration
112
{
113
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
114
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
115
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
116
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
117
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
118
    private const DEFAULT_BANNER = <<<'BANNER'
119
Generated by Humbug Box %s.
120
121
@link https://github.com/humbug/box
122
BANNER;
123
    private const FILES_SETTINGS = [
124
        'directories',
125
        'finder',
126
    ];
127
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
128
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
129
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
130
131
    private const DEFAULT_IGNORED_ANNOTATIONS = [
132
        'abstract',
133
        'access',
134
        'annotation',
135
        'api',
136
        'attribute',
137
        'attributes',
138
        'author',
139
        'category',
140
        'code',
141
        'codecoverageignore',
142
        'codecoverageignoreend',
143
        'codecoverageignorestart',
144
        'copyright',
145
        'deprec',
146
        'deprecated',
147
        'endcode',
148
        'example',
149
        'exception',
150
        'filesource',
151
        'final',
152
        'fixme',
153
        'global',
154
        'ignore',
155
        'ingroup',
156
        'inheritdoc',
157
        'internal',
158
        'license',
159
        'link',
160
        'magic',
161
        'method',
162
        'name',
163
        'override',
164
        'package',
165
        'package_version',
166
        'param',
167
        'private',
168
        'property',
169
        'required',
170
        'return',
171
        'see',
172
        'since',
173
        'static',
174
        'staticvar',
175
        'subpackage',
176
        'suppresswarnings',
177
        'target',
178
        'throw',
179
        'throws',
180
        'todo',
181
        'tutorial',
182
        'usedby',
183
        'uses',
184
        'var',
185
        'version',
186
    ];
187
188
    private const ALGORITHM_KEY = 'algorithm';
189
    private const ALIAS_KEY = 'alias';
190
    private const ANNOTATIONS_KEY = 'annotations';
191
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
192
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
193
    private const BANNER_KEY = 'banner';
194
    private const BANNER_FILE_KEY = 'banner-file';
195
    private const BASE_PATH_KEY = 'base-path';
196
    private const BLACKLIST_KEY = 'blacklist';
197
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
198
    private const CHMOD_KEY = 'chmod';
199
    private const COMPACTORS_KEY = 'compactors';
200
    private const COMPRESSION_KEY = 'compression';
201
    private const DATETIME_KEY = 'datetime';
202
    private const DATETIME_FORMAT_KEY = 'datetime-format';
203
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
204
    private const DIRECTORIES_KEY = 'directories';
205
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
206
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
207
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
208
    private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files';
209
    private const FILES_KEY = 'files';
210
    private const FILES_BIN_KEY = 'files-bin';
211
    private const FINDER_KEY = 'finder';
212
    private const FINDER_BIN_KEY = 'finder-bin';
213
    private const GIT_KEY = 'git';
214
    private const GIT_COMMIT_KEY = 'git-commit';
215
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
216
    private const GIT_TAG_KEY = 'git-tag';
217
    private const GIT_VERSION_KEY = 'git-version';
218
    private const INTERCEPT_KEY = 'intercept';
219
    private const KEY_KEY = 'key';
220
    private const KEY_PASS_KEY = 'key-pass';
221
    private const MAIN_KEY = 'main';
222
    private const MAP_KEY = 'map';
223
    private const METADATA_KEY = 'metadata';
224
    private const OUTPUT_KEY = 'output';
225
    private const PHP_SCOPER_KEY = 'php-scoper';
226
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
227
    private const REPLACEMENTS_KEY = 'replacements';
228
    private const SHEBANG_KEY = 'shebang';
229
    private const STUB_KEY = 'stub';
230
231
    private $file;
232
    private $fileMode;
233
    private $alias;
234
    private $basePath;
235
    private $composerJson;
236
    private $composerLock;
237
    private $files;
238
    private $binaryFiles;
239
    private $autodiscoveredFiles;
240
    private $dumpAutoload;
241
    private $excludeComposerFiles;
242
    private $excludeDevFiles;
243
    private $compactors;
244
    private $compressionAlgorithm;
245
    private $mainScriptPath;
246
    private $mainScriptContents;
247
    private $fileMapper;
248
    private $metadata;
249
    private $tmpOutputPath;
250
    private $outputPath;
251
    private $privateKeyPassphrase;
252
    private $privateKeyPath;
253
    private $promptForPrivateKey;
254
    private $processedReplacements;
255
    private $shebang;
256
    private $signingAlgorithm;
257
    private $stubBannerContents;
258
    private $stubBannerPath;
259
    private $stubPath;
260
    private $isInterceptFileFuncs;
261
    private $isStubGenerated;
262
    private $checkRequirements;
263
    private $warnings;
264
    private $recommendations;
265
266
    public static function create(?string $file, stdClass $raw): self
267
    {
268
        $logger = new ConfigurationLogger();
269
270
        $basePath = self::retrieveBasePath($file, $raw, $logger);
271
272
        $composerFiles = self::retrieveComposerFiles($basePath);
273
274
        $dumpAutoload = self::retrieveDumpAutoload($raw, $composerFiles, $logger);
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
            $logger
368
        );
369
        $binaryFilesAggregate = self::collectBinaryFiles(
370
            $raw,
371
            $basePath,
372
            $blacklistFilter,
373
            $excludedPaths,
374
            $alwaysExcludedPaths,
375
            $devPackages,
376
            $logger
377
        );
378
379
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
380
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
381
382
        $fileMode = self::retrieveFileMode($raw, $logger);
383
384
        $map = self::retrieveMap($raw, $logger);
385
        $fileMapper = new MapFile($basePath, $map);
386
387
        $metadata = self::retrieveMetadata($raw, $logger);
388
389
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
390
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
391
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
392
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
393
394
        $replacements = self::retrieveReplacements($raw, $file, $logger);
395
396
        return new self(
397
            $file,
398
            $alias,
399
            $basePath,
400
            $composerFiles->getComposerJson(),
401
            $composerFiles->getComposerLock(),
402
            $filesAggregate,
403
            $binaryFilesAggregate,
404
            $autodiscoverFiles || $forceFilesAutodiscovery,
405
            $dumpAutoload,
406
            $excludeComposerFiles,
407
            $excludeDevPackages,
408
            $compactors,
409
            $compressionAlgorithm,
410
            $fileMode,
411
            $mainScriptPath,
412
            $mainScriptContents,
413
            $fileMapper,
414
            $metadata,
415
            $tmpOutputPath,
416
            $outputPath,
417
            $privateKeyPassphrase,
418
            $privateKeyPath,
419
            $promptForPrivateKey,
420
            $replacements,
421
            $shebang,
422
            $signingAlgorithm,
423
            $stubBannerContents,
424
            $stubBannerPath,
425
            $stubPath,
426
            $isInterceptsFileFunctions,
427
            $isStubGenerated,
428
            $checkRequirements,
429
            $logger->getWarnings(),
430
            $logger->getRecommendations()
431
        );
432
    }
433
434
    /**
435
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
436
     *                                            path relative to it (the base path)
437
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
438
     *                                            string and the second element its decoded contents as an
439
     *                                            associative array.
440
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
441
     *                                            string and the second element its decoded contents as an
442
     *                                            associative array.
443
     * @param SplFileInfo[] $files                List of files
444
     * @param SplFileInfo[] $binaryFiles          List of binary files
445
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
446
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
447
     *                                            installed.json should be removed from the PHAR
448
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
449
     * @param null|int      $fileMode             File mode in octal form
450
     * @param string        $mainScriptPath       The main script file path
451
     * @param string        $mainScriptContents   The processed content of the main script file
452
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
453
     * @param mixed         $metadata             The PHAR Metadata
454
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
455
     * @param array         $replacements         The processed list of replacement placeholders and their values
456
     * @param null|string   $shebang              The shebang line
457
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
458
     * @param null|string   $stubBannerContents   The stub banner comment
459
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
460
     * @param null|string   $stubPath             The PHAR stub file path
461
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
462
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
463
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
464
     *                                            running
465
     * @param string[]      $warnings
466
     * @param string[]      $recommendations
467
     */
468
    private function __construct(
469
        ?string $file,
470
        string $alias,
471
        string $basePath,
472
        ComposerFile $composerJson,
473
        ComposerFile $composerLock,
474
        array $files,
475
        array $binaryFiles,
476
        bool $autodiscoveredFiles,
477
        bool $dumpAutoload,
478
        bool $excludeComposerFiles,
479
        bool $excludeDevPackages,
480
        Compactors $compactors,
481
        ?int $compressionAlgorithm,
482
        ?int $fileMode,
483
        ?string $mainScriptPath,
484
        ?string $mainScriptContents,
485
        MapFile $fileMapper,
486
        $metadata,
487
        string $tmpOutputPath,
488
        string $outputPath,
489
        ?string $privateKeyPassphrase,
490
        ?string $privateKeyPath,
491
        bool $promptForPrivateKey,
492
        array $replacements,
493
        ?string $shebang,
494
        int $signingAlgorithm,
495
        ?string $stubBannerContents,
496
        ?string $stubBannerPath,
497
        ?string $stubPath,
498
        bool $isInterceptFileFuncs,
499
        bool $isStubGenerated,
500
        bool $checkRequirements,
501
        array $warnings,
502
        array $recommendations
503
    ) {
504
        Assertion::nullOrInArray(
505
            $compressionAlgorithm,
506
            get_phar_compression_algorithms(),
507
            sprintf(
508
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
509
                implode('", "', array_keys(get_phar_compression_algorithms()))
510
            )
511
        );
512
513
        if (null === $mainScriptPath) {
514
            Assertion::null($mainScriptContents);
515
        } else {
516
            Assertion::notNull($mainScriptContents);
517
        }
518
519
        $this->file = $file;
520
        $this->alias = $alias;
521
        $this->basePath = $basePath;
522
        $this->composerJson = $composerJson;
523
        $this->composerLock = $composerLock;
524
        $this->files = $files;
525
        $this->binaryFiles = $binaryFiles;
526
        $this->autodiscoveredFiles = $autodiscoveredFiles;
527
        $this->dumpAutoload = $dumpAutoload;
528
        $this->excludeComposerFiles = $excludeComposerFiles;
529
        $this->excludeDevFiles = $excludeDevPackages;
530
        $this->compactors = $compactors;
531
        $this->compressionAlgorithm = $compressionAlgorithm;
532
        $this->fileMode = $fileMode;
533
        $this->mainScriptPath = $mainScriptPath;
534
        $this->mainScriptContents = $mainScriptContents;
535
        $this->fileMapper = $fileMapper;
536
        $this->metadata = $metadata;
537
        $this->tmpOutputPath = $tmpOutputPath;
538
        $this->outputPath = $outputPath;
539
        $this->privateKeyPassphrase = $privateKeyPassphrase;
540
        $this->privateKeyPath = $privateKeyPath;
541
        $this->promptForPrivateKey = $promptForPrivateKey;
542
        $this->processedReplacements = $replacements;
543
        $this->shebang = $shebang;
544
        $this->signingAlgorithm = $signingAlgorithm;
545
        $this->stubBannerContents = $stubBannerContents;
546
        $this->stubBannerPath = $stubBannerPath;
547
        $this->stubPath = $stubPath;
548
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
549
        $this->isStubGenerated = $isStubGenerated;
550
        $this->checkRequirements = $checkRequirements;
551
        $this->warnings = $warnings;
552
        $this->recommendations = $recommendations;
553
    }
554
555
    public function export(): string
556
    {
557
        $exportedConfig = clone $this;
558
559
        $basePath = $exportedConfig->basePath;
560
561
        /**
562
         * @param null|SplFileInfo|string $path
563
         */
564
        $normalizePath = static function ($path) use ($basePath): ?string {
565
            if (null === $path) {
566
                return null;
567
            }
568
569
            if ($path instanceof SplFileInfo) {
570
                $path = $path->getPathname();
571
            }
572
573
            return make_path_relative($path, $basePath);
574
        };
575
576
        $normalizeProperty = static function (&$property) use ($normalizePath): void {
577
            $property = $normalizePath($property);
578
        };
579
580
        $normalizeFiles = static function (&$files) use ($normalizePath): void {
581
            $files = array_map($normalizePath, $files);
582
            sort($files, SORT_STRING);
583
        };
584
585
        $normalizeFiles($exportedConfig->files);
586
        $normalizeFiles($exportedConfig->binaryFiles);
587
588
        $exportedConfig->composerJson = new ComposerFile(
589
            $normalizePath($exportedConfig->composerJson->getPath()),
590
            $exportedConfig->composerJson->getDecodedContents()
591
        );
592
        $exportedConfig->composerLock = new ComposerFile(
593
            $normalizePath($exportedConfig->composerLock->getPath()),
594
            $exportedConfig->composerLock->getDecodedContents()
595
        );
596
597
        $normalizeProperty($exportedConfig->file);
598
        $normalizeProperty($exportedConfig->mainScriptPath);
599
        $normalizeProperty($exportedConfig->tmpOutputPath);
600
        $normalizeProperty($exportedConfig->outputPath);
601
        $normalizeProperty($exportedConfig->privateKeyPath);
602
        $normalizeProperty($exportedConfig->stubBannerPath);
603
        $normalizeProperty($exportedConfig->stubPath);
604
605
        $exportedConfig->compressionAlgorithm = array_flip(get_phar_compression_algorithms())[$exportedConfig->compressionAlgorithm ?? Phar::NONE];
606
        $exportedConfig->signingAlgorithm = array_flip(get_phar_signing_algorithms())[$exportedConfig->signingAlgorithm];
607
        $exportedConfig->compactors = array_map(
608
            static function (Compactor $compactor): string {
609
                return get_class(
610
                    $compactor instanceof CompactorProxy
611
                    ? $compactor->getCompactor()
612
                    : $compactor
613
                );
614
            },
615
            $exportedConfig->compactors
0 ignored issues
show
Bug introduced by
$exportedConfig->compactors of type KevinGH\Box\Compactor\Compactors is incompatible with the type array expected by parameter $arr1 of array_map(). ( Ignorable by Annotation )

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

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