Completed
Pull Request — master (#406)
by Théo
02:48
created

Configuration::createCompactors()   B

Complexity

Conditions 6
Paths 1

Size

Total Lines 45
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

605
        $exportedConfig->compactors = array_map('get_class', /** @scrutinizer ignore-type */ $exportedConfig->compactors);
Loading history...
606
        $exportedConfig->fileMode = '0'.decoct($exportedConfig->fileMode);
607
608
        $cloner = new VarCloner();
609
        $cloner->setMaxItems(-1);
610
        $cloner->setMaxString(-1);
611
612
        $splInfoCaster = static function (SplFileInfo $fileInfo) use ($normalizePath): array {
613
            return [$normalizePath($fileInfo)];
614
        };
615
616
        $cloner->addCasters([
617
            SplFileInfo::class => $splInfoCaster,
618
            SymfonySplFileInfo::class => $splInfoCaster,
619
        ]);
620
621
        return (new CliDumper())->dump(
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Symfony\Compo...$exportedConfig), true) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
622
            $cloner->cloneVar($exportedConfig),
623
            true
624
        );
625
    }
626
627
    public function getConfigurationFile(): ?string
628
    {
629
        return $this->file;
630
    }
631
632
    public function getAlias(): string
633
    {
634
        return $this->alias;
635
    }
636
637
    public function getBasePath(): string
638
    {
639
        return $this->basePath;
640
    }
641
642
    public function getComposerJson(): ?string
643
    {
644
        return $this->composerJson->getPath();
645
    }
646
647
    public function getDecodedComposerJsonContents(): ?array
648
    {
649
        return null === $this->composerJson->getPath() ? null : $this->composerJson->getDecodedContents();
650
    }
651
652
    public function getComposerLock(): ?string
653
    {
654
        return $this->composerLock->getPath();
655
    }
656
657
    public function getDecodedComposerLockContents(): ?array
658
    {
659
        return null === $this->composerLock->getPath() ? null : $this->composerLock->getDecodedContents();
660
    }
661
662
    /**
663
     * @return SplFileInfo[]
664
     */
665
    public function getFiles(): array
666
    {
667
        return $this->files;
668
    }
669
670
    /**
671
     * @return SplFileInfo[]
672
     */
673
    public function getBinaryFiles(): array
674
    {
675
        return $this->binaryFiles;
676
    }
677
678
    public function hasAutodiscoveredFiles(): bool
679
    {
680
        return $this->autodiscoveredFiles;
681
    }
682
683
    public function dumpAutoload(): bool
684
    {
685
        return $this->dumpAutoload;
686
    }
687
688
    public function excludeComposerFiles(): bool
689
    {
690
        return $this->excludeComposerFiles;
691
    }
692
693
    public function excludeDevFiles(): bool
694
    {
695
        return $this->excludeDevFiles;
696
    }
697
698
    public function getCompactors(): Compactors
699
    {
700
        return $this->compactors;
701
    }
702
703
    public function getCompressionAlgorithm(): ?int
704
    {
705
        return $this->compressionAlgorithm;
706
    }
707
708
    public function getFileMode(): ?int
709
    {
710
        return $this->fileMode;
711
    }
712
713
    public function hasMainScript(): bool
714
    {
715
        return null !== $this->mainScriptPath;
716
    }
717
718
    public function getMainScriptPath(): string
719
    {
720
        Assertion::notNull(
721
            $this->mainScriptPath,
722
            'Cannot retrieve the main script path: no main script configured.'
723
        );
724
725
        return $this->mainScriptPath;
726
    }
727
728
    public function getMainScriptContents(): string
729
    {
730
        Assertion::notNull(
731
            $this->mainScriptPath,
732
            'Cannot retrieve the main script contents: no main script configured.'
733
        );
734
735
        return $this->mainScriptContents;
736
    }
737
738
    public function checkRequirements(): bool
739
    {
740
        return $this->checkRequirements;
741
    }
742
743
    public function getTmpOutputPath(): string
744
    {
745
        return $this->tmpOutputPath;
746
    }
747
748
    public function getOutputPath(): string
749
    {
750
        return $this->outputPath;
751
    }
752
753
    public function getFileMapper(): MapFile
754
    {
755
        return $this->fileMapper;
756
    }
757
758
    /**
759
     * @return mixed
760
     */
761
    public function getMetadata()
762
    {
763
        return $this->metadata;
764
    }
765
766
    public function getPrivateKeyPassphrase(): ?string
767
    {
768
        return $this->privateKeyPassphrase;
769
    }
770
771
    public function getPrivateKeyPath(): ?string
772
    {
773
        return $this->privateKeyPath;
774
    }
775
776
    /**
777
     * @deprecated Use promptForPrivateKey() instead
778
     */
779
    public function isPrivateKeyPrompt(): bool
780
    {
781
        return $this->promptForPrivateKey;
782
    }
783
784
    public function promptForPrivateKey(): bool
785
    {
786
        return $this->promptForPrivateKey;
787
    }
788
789
    /**
790
     * @return scalar[]
791
     */
792
    public function getReplacements(): array
793
    {
794
        return $this->processedReplacements;
795
    }
796
797
    public function getShebang(): ?string
798
    {
799
        return $this->shebang;
800
    }
801
802
    public function getSigningAlgorithm(): int
803
    {
804
        return $this->signingAlgorithm;
805
    }
806
807
    public function getStubBannerContents(): ?string
808
    {
809
        return $this->stubBannerContents;
810
    }
811
812
    public function getStubBannerPath(): ?string
813
    {
814
        return $this->stubBannerPath;
815
    }
816
817
    public function getStubPath(): ?string
818
    {
819
        return $this->stubPath;
820
    }
821
822
    public function isInterceptFileFuncs(): bool
823
    {
824
        return $this->isInterceptFileFuncs;
825
    }
826
827
    public function isStubGenerated(): bool
828
    {
829
        return $this->isStubGenerated;
830
    }
831
832
    /**
833
     * @return string[]
834
     */
835
    public function getWarnings(): array
836
    {
837
        return $this->warnings;
838
    }
839
840
    /**
841
     * @return string[]
842
     */
843
    public function getRecommendations(): array
844
    {
845
        return $this->recommendations;
846
    }
847
848
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
849
    {
850
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
851
852
        if (false === isset($raw->{self::ALIAS_KEY})) {
853
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
854
        }
855
856
        $alias = trim($raw->{self::ALIAS_KEY});
857
858
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
859
860
        if ($userStubUsed) {
861
            $logger->addWarning(
862
                sprintf(
863
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
864
                    self::ALIAS_KEY
865
                )
866
            );
867
        }
868
869
        return $alias;
870
    }
871
872
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
873
    {
874
        if (null === $file) {
875
            return getcwd();
876
        }
877
878
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
879
            return realpath(dirname($file));
880
        }
881
882
        $basePath = trim($raw->{self::BASE_PATH_KEY});
883
884
        Assertion::directory(
885
            $basePath,
886
            'The base path "%s" is not a directory or does not exist.'
887
        );
888
889
        $basePath = realpath($basePath);
890
        $defaultPath = realpath(dirname($file));
891
892
        if ($basePath === $defaultPath) {
893
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
894
        }
895
896
        return $basePath;
897
    }
898
899
    /**
900
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
901
     */
902
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
903
    {
904
        if (null === $file) {
905
            return true;
906
        }
907
908
        $associativeRaw = (array) $raw;
909
910
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
911
    }
912
913
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
914
    {
915
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
916
917
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
918
    }
919
920
    private static function retrieveBlacklistFilter(
921
        stdClass $raw,
922
        string $basePath,
923
        ConfigurationLogger $logger,
924
        ?string ...$excludedPaths
925
    ): array {
926
        $blacklist = array_flip(
927
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
928
        );
929
930
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
931
            if ($file->isLink()) {
932
                return false;
933
            }
934
935
            if (false === $file->getRealPath()) {
936
                return false;
937
            }
938
939
            if (array_key_exists($file->getRealPath(), $blacklist)) {
940
                return false;
941
            }
942
943
            return null;
944
        };
945
946
        return [array_keys($blacklist), $blacklistFilter];
947
    }
948
949
    /**
950
     * @param null[]|string[] $excludedPaths
951
     *
952
     * @return string[]
953
     */
954
    private static function retrieveBlacklist(
955
        stdClass $raw,
956
        string $basePath,
957
        ConfigurationLogger $logger,
958
        ?string ...$excludedPaths
959
    ): array {
960
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
961
962
        $normalizedBlacklist = array_map(
963
            static function (string $excludedPath) use ($basePath): string {
964
                return self::normalizePath($excludedPath, $basePath);
965
            },
966
            array_filter($excludedPaths)
967
        );
968
969
        /** @var string[] $blacklist */
970
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
971
972
        foreach ($blacklist as $file) {
973
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
974
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
975
        }
976
977
        return array_unique($normalizedBlacklist);
978
    }
979
980
    /**
981
     * @param string[] $excludedPaths
982
     * @param string[] $alwaysExcludedPaths
983
     * @param string[] $devPackages
984
     *
985
     * @return SplFileInfo[]
986
     */
987
    private static function collectFiles(
988
        stdClass $raw,
989
        string $basePath,
990
        ?string $mainScriptPath,
991
        Closure $blacklistFilter,
992
        array $excludedPaths,
993
        array $alwaysExcludedPaths,
994
        array $devPackages,
995
        ComposerFiles $composerFiles,
996
        bool $autodiscoverFiles,
997
        bool $forceFilesAutodiscovery,
998
        ConfigurationLogger $logger
999
    ): array {
1000
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)];
1001
1002
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
1003
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
1004
                $basePath,
1005
                $composerFiles->getComposerJson()->getDecodedContents(),
1006
                $devPackages,
1007
                $composerFiles->getPaths(),
1008
                $excludedPaths
1009
            );
1010
1011
            $files[] = self::wrapInSplFileInfo($filesToAppend);
1012
1013
            $files[] = self::retrieveAllFiles(
1014
                $basePath,
1015
                $directories,
1016
                $mainScriptPath,
1017
                $blacklistFilter,
1018
                $excludedPaths,
1019
                $devPackages
1020
            );
1021
        }
1022
1023
        if (false === $autodiscoverFiles) {
1024
            $files[] = self::retrieveDirectories(
1025
                $raw,
1026
                self::DIRECTORIES_KEY,
1027
                $basePath,
1028
                $blacklistFilter,
1029
                $excludedPaths,
1030
                $logger
1031
            );
1032
1033
            $filesFromFinders = self::retrieveFilesFromFinders(
1034
                $raw,
1035
                self::FINDER_KEY,
1036
                $basePath,
1037
                $blacklistFilter,
1038
                $devPackages,
1039
                $logger
1040
            );
1041
1042
            foreach ($filesFromFinders as $filesFromFinder) {
1043
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
1044
                $files[] = $filesFromFinder;
1045
            }
1046
1047
            $files[] = self::wrapInSplFileInfo($composerFiles->getPaths());
1048
        }
1049
1050
        return self::retrieveFilesAggregate(...$files);
1051
    }
1052
1053
    /**
1054
     * @param string[] $excludedPaths
1055
     * @param string[] $alwaysExcludedPaths
1056
     * @param string[] $devPackages
1057
     *
1058
     * @return SplFileInfo[]
1059
     */
1060
    private static function collectBinaryFiles(
1061
        stdClass $raw,
1062
        string $basePath,
1063
        Closure $blacklistFilter,
1064
        array $excludedPaths,
1065
        array $alwaysExcludedPaths,
1066
        array $devPackages,
1067
        ConfigurationLogger $logger
1068
    ): array {
1069
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, ComposerFiles::createEmpty(), $alwaysExcludedPaths, $logger);
1070
1071
        $binaryDirectories = self::retrieveDirectories(
1072
            $raw,
1073
            self::DIRECTORIES_BIN_KEY,
1074
            $basePath,
1075
            $blacklistFilter,
1076
            $excludedPaths,
1077
            $logger
1078
        );
1079
1080
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
1081
            $raw,
1082
            self::FINDER_BIN_KEY,
1083
            $basePath,
1084
            $blacklistFilter,
1085
            $devPackages,
1086
            $logger
1087
        );
1088
1089
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
1090
    }
1091
1092
    /**
1093
     * @param string[] $excludedFiles
1094
     *
1095
     * @return SplFileInfo[]
1096
     */
1097
    private static function retrieveFiles(
1098
        stdClass $raw,
1099
        string $key,
1100
        string $basePath,
1101
        ComposerFiles $composerFiles,
1102
        array $excludedFiles,
1103
        ConfigurationLogger $logger
1104
    ): array {
1105
        self::checkIfDefaultValue($logger, $raw, $key, []);
1106
1107
        $excludedFiles = array_flip($excludedFiles);
1108
        $files = array_filter([
1109
            $composerFiles->getComposerJson()->getPath(),
1110
            $composerFiles->getComposerLock()->getPath(),
1111
        ]);
1112
1113
        if (false === isset($raw->{$key})) {
1114
            return self::wrapInSplFileInfo($files);
1115
        }
1116
1117
        if ([] === (array) $raw->{$key}) {
1118
            return self::wrapInSplFileInfo($files);
1119
        }
1120
1121
        $files = array_merge((array) $raw->{$key}, $files);
1122
1123
        Assertion::allString($files);
1124
1125
        $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo {
1126
            $file = self::normalizePath($file, $basePath);
1127
1128
            Assertion::false(
1129
                is_link($file),
1130
                sprintf(
1131
                    'Cannot add the link "%s": links are not supported.',
1132
                    $file
1133
                )
1134
            );
1135
1136
            Assertion::file(
1137
                $file,
1138
                sprintf(
1139
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1140
                    $key
1141
                )
1142
            );
1143
1144
            return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file);
1145
        };
1146
1147
        return array_filter(array_map($normalizePath, $files));
1148
    }
1149
1150
    /**
1151
     * @param string   $key           Config property name
1152
     * @param string[] $excludedPaths
1153
     *
1154
     * @return iterable|SplFileInfo[]
1155
     */
1156
    private static function retrieveDirectories(
1157
        stdClass $raw,
1158
        string $key,
1159
        string $basePath,
1160
        Closure $blacklistFilter,
1161
        array $excludedPaths,
1162
        ConfigurationLogger $logger
1163
    ): iterable {
1164
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1165
1166
        if ([] !== $directories) {
1167
            $finder = Finder::create()
1168
                ->files()
1169
                ->filter($blacklistFilter)
1170
                ->ignoreVCS(true)
1171
                ->in($directories)
1172
            ;
1173
1174
            foreach ($excludedPaths as $excludedPath) {
1175
                $finder->notPath($excludedPath);
1176
            }
1177
1178
            return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[]|iterable.
Loading history...
1179
        }
1180
1181
        return [];
1182
    }
1183
1184
    /**
1185
     * @param string[] $devPackages
1186
     *
1187
     * @return iterable[]|SplFileInfo[][]
1188
     */
1189
    private static function retrieveFilesFromFinders(
1190
        stdClass $raw,
1191
        string $key,
1192
        string $basePath,
1193
        Closure $blacklistFilter,
1194
        array $devPackages,
1195
        ConfigurationLogger $logger
1196
    ): array {
1197
        self::checkIfDefaultValue($logger, $raw, $key, []);
1198
1199
        if (false === isset($raw->{$key})) {
1200
            return [];
1201
        }
1202
1203
        $finder = $raw->{$key};
1204
1205
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1206
    }
1207
1208
    /**
1209
     * @param iterable[]|SplFileInfo[][] $fileIterators
1210
     *
1211
     * @return SplFileInfo[]
1212
     */
1213
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1214
    {
1215
        $files = [];
1216
1217
        foreach ($fileIterators as $fileIterator) {
1218
            foreach ($fileIterator as $file) {
1219
                $files[(string) $file] = $file;
1220
            }
1221
        }
1222
1223
        return array_values($files);
1224
    }
1225
1226
    /**
1227
     * @param string[] $devPackages
1228
     *
1229
     * @return Finder[]|SplFileInfo[][]
1230
     */
1231
    private static function processFinders(
1232
        array $findersConfig,
1233
        string $basePath,
1234
        Closure $blacklistFilter,
1235
        array $devPackages
1236
    ): array {
1237
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1238
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1239
        };
1240
1241
        return array_map($processFinderConfig, $findersConfig);
1242
    }
1243
1244
    /**
1245
     * @param string[] $devPackages
1246
     *
1247
     * @return Finder|SplFileInfo[]
1248
     */
1249
    private static function processFinder(
1250
        stdClass $config,
1251
        string $basePath,
1252
        Closure $blacklistFilter,
1253
        array $devPackages
1254
    ): Finder {
1255
        $finder = Finder::create()
1256
            ->files()
1257
            ->filter($blacklistFilter)
1258
            ->filter(
1259
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1260
                    foreach ($devPackages as $devPackage) {
1261
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1262
                            // File belongs to the dev package
1263
                            return false;
1264
                        }
1265
                    }
1266
1267
                    return true;
1268
                }
1269
            )
1270
            ->ignoreVCS(true)
1271
        ;
1272
1273
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1274
            $normalizedConfig = [];
1275
1276
            foreach ($config as $method => $arguments) {
1277
                $method = trim($method);
1278
                $arguments = (array) $arguments;
1279
1280
                Assertion::methodExists(
1281
                    $method,
1282
                    $finder,
1283
                    'The method "Finder::%s" does not exist.'
1284
                );
1285
1286
                $normalizedConfig[$method] = $arguments;
1287
            }
1288
1289
            krsort($normalizedConfig);
1290
1291
            return $normalizedConfig;
1292
        })((array) $config, $finder);
1293
1294
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1295
            $directory = self::normalizePath($directory, $basePath);
1296
1297
            Assertion::false(
1298
                is_link($directory),
1299
                sprintf(
1300
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1301
                    $directory
1302
                )
1303
            );
1304
1305
            Assertion::directory($directory);
1306
1307
            return $directory;
1308
        };
1309
1310
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1311
            if (null === $fileOrDirectory) {
1312
                return;
1313
            }
1314
1315
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
1316
1317
            Assertion::false(
1318
                is_link($fileOrDirectory),
1319
                sprintf(
1320
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1321
                    $fileOrDirectory
1322
                )
1323
            );
1324
1325
            Assertion::true(
1326
                file_exists($fileOrDirectory),
1327
                sprintf(
1328
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1329
                    $fileOrDirectory
1330
                )
1331
            );
1332
1333
            if (false === is_file($fileOrDirectory)) {
1334
                Assertion::directory($fileOrDirectory);
1335
            } else {
1336
                Assertion::file($fileOrDirectory);
1337
            }
1338
1339
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1340
                $fileOrDirectory = null;
1341
            }
1342
        };
1343
1344
        foreach ($normalizedConfig as $method => $arguments) {
1345
            if ('in' === $method) {
1346
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1347
            }
1348
1349
            if ('exclude' === $method) {
1350
                $arguments = array_unique(array_map('trim', $arguments));
1351
            }
1352
1353
            if ('append' === $method) {
1354
                array_walk($arguments, $normalizeFileOrDirectory);
1355
1356
                $arguments = [array_filter($arguments)];
1357
            }
1358
1359
            foreach ($arguments as $argument) {
1360
                $finder->$method($argument);
1361
            }
1362
        }
1363
1364
        return $finder;
1365
    }
1366
1367
    /**
1368
     * @param string[] $devPackages
1369
     * @param string[] $filesToAppend
1370
     *
1371
     * @return string[][]
1372
     */
1373
    private static function retrieveAllDirectoriesToInclude(
1374
        string $basePath,
1375
        ?array $decodedJsonContents,
1376
        array $devPackages,
1377
        array $filesToAppend,
1378
        array $excludedPaths
1379
    ): array {
1380
        $toString = static function ($file): string {
1381
            // @param string|SplFileInfo $file
1382
            return (string) $file;
1383
        };
1384
1385
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1386
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1387
        } else {
1388
            $vendorDir = self::normalizePath('vendor', $basePath);
1389
        }
1390
1391
        if (file_exists($vendorDir)) {
1392
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1393
            // dependencies are included in the `composer.json`
1394
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1395
1396
            if (file_exists($installedJsonFiles)) {
1397
                $filesToAppend[] = $installedJsonFiles;
1398
            }
1399
1400
            $vendorPackages = toArray(values(map(
1401
                $toString,
1402
                Finder::create()
1403
                    ->in($vendorDir)
1404
                    ->directories()
1405
                    ->depth(1)
1406
                    ->ignoreUnreadableDirs()
1407
                    ->filter(
1408
                        static function (SplFileInfo $fileInfo): ?bool {
1409
                            if ($fileInfo->isLink()) {
1410
                                return false;
1411
                            }
1412
1413
                            return null;
1414
                        }
1415
                    )
1416
            )));
1417
1418
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1419
1420
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1421
                $files = toArray(values(map(
1422
                    $toString,
1423
                    Finder::create()
1424
                        ->in($basePath)
1425
                        ->files()
1426
                        ->depth(0)
1427
                )));
1428
1429
                $directories = toArray(values(map(
1430
                    $toString,
1431
                    Finder::create()
1432
                        ->in($basePath)
1433
                        ->notPath('vendor')
1434
                        ->directories()
1435
                        ->depth(0)
1436
                )));
1437
1438
                return [
1439
                    array_merge(
1440
                        array_diff($files, $excludedPaths),
1441
                        $filesToAppend
1442
                    ),
1443
                    array_merge(
1444
                        array_diff($directories, $excludedPaths),
1445
                        $vendorPackages
1446
                    ),
1447
                ];
1448
            }
1449
1450
            $paths = $vendorPackages;
1451
        } else {
1452
            $paths = [];
1453
        }
1454
1455
        $autoload = $decodedJsonContents['autoload'] ?? [];
1456
1457
        if (array_key_exists('psr-4', $autoload)) {
1458
            foreach ($autoload['psr-4'] as $path) {
1459
                /** @var string|string[] $path */
1460
                $composerPaths = (array) $path;
1461
1462
                foreach ($composerPaths as $composerPath) {
1463
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1464
                }
1465
            }
1466
        }
1467
1468
        if (array_key_exists('psr-0', $autoload)) {
1469
            foreach ($autoload['psr-0'] as $path) {
1470
                /** @var string|string[] $path */
1471
                $composerPaths = (array) $path;
1472
1473
                foreach ($composerPaths as $composerPath) {
1474
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1475
                }
1476
            }
1477
        }
1478
1479
        if (array_key_exists('classmap', $autoload)) {
1480
            foreach ($autoload['classmap'] as $path) {
1481
                // @var string $path
1482
                $paths[] = $path;
1483
            }
1484
        }
1485
1486
        $normalizePath = static function (string $path) use ($basePath): string {
1487
            return is_absolute_path($path)
1488
                ? canonicalize($path)
1489
                : self::normalizePath(trim($path, '/ '), $basePath)
1490
            ;
1491
        };
1492
1493
        if (array_key_exists('files', $autoload)) {
1494
            foreach ($autoload['files'] as $path) {
1495
                // @var string $path
1496
                $path = $normalizePath($path);
1497
1498
                Assertion::file($path);
1499
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1500
1501
                $filesToAppend[] = $path;
1502
            }
1503
        }
1504
1505
        $files = $filesToAppend;
1506
        $directories = [];
1507
1508
        foreach ($paths as $path) {
1509
            $path = $normalizePath($path);
1510
1511
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1512
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1513
1514
            if (is_file($path)) {
1515
                $files[] = $path;
1516
            } else {
1517
                $directories[] = $path;
1518
            }
1519
        }
1520
1521
        [$files, $directories] = [
1522
            array_unique($files),
1523
            array_unique($directories),
1524
        ];
1525
1526
        return [
1527
            array_diff($files, $excludedPaths),
1528
            array_diff($directories, $excludedPaths),
1529
        ];
1530
    }
1531
1532
    /**
1533
     * @param string[] $files
1534
     * @param string[] $directories
1535
     * @param string[] $excludedPaths
1536
     * @param string[] $devPackages
1537
     *
1538
     * @return SplFileInfo[]
1539
     */
1540
    private static function retrieveAllFiles(
1541
        string $basePath,
1542
        array $directories,
1543
        ?string $mainScriptPath,
1544
        Closure $blacklistFilter,
1545
        array $excludedPaths,
1546
        array $devPackages
1547
    ): iterable {
1548
        if ([] === $directories) {
1549
            return [];
1550
        }
1551
1552
        $relativeDevPackages = array_map(
1553
            static function (string $packagePath) use ($basePath): string {
1554
                return make_path_relative($packagePath, $basePath);
1555
            },
1556
            $devPackages
1557
        );
1558
1559
        $finder = Finder::create()
1560
            ->files()
1561
            ->filter($blacklistFilter)
1562
            ->exclude($relativeDevPackages)
1563
            ->ignoreVCS(true)
1564
            ->ignoreDotFiles(true)
1565
            // Remove build files
1566
            ->notName('composer.json')
1567
            ->notName('composer.lock')
1568
            ->notName('Makefile')
1569
            ->notName('Vagrantfile')
1570
            ->notName('phpstan*.neon*')
1571
            ->notName('infection*.json*')
1572
            ->notName('humbug*.json*')
1573
            ->notName('easy-coding-standard.neon*')
1574
            ->notName('phpbench.json*')
1575
            ->notName('phpcs.xml*')
1576
            ->notName('psalm.xml*')
1577
            ->notName('scoper.inc*')
1578
            ->notName('box*.json*')
1579
            ->notName('phpdoc*.xml*')
1580
            ->notName('codecov.yml*')
1581
            ->notName('Dockerfile')
1582
            ->exclude('build')
1583
            ->exclude('dist')
1584
            ->exclude('example')
1585
            ->exclude('examples')
1586
            // Remove documentation
1587
            ->notName('*.md')
1588
            ->notName('*.rst')
1589
            ->notName('/^readme((?!\.php)(\..*+))?$/i')
1590
            ->notName('/^upgrade((?!\.php)(\..*+))?$/i')
1591
            ->notName('/^contributing((?!\.php)(\..*+))?$/i')
1592
            ->notName('/^changelog((?!\.php)(\..*+))?$/i')
1593
            ->notName('/^authors?((?!\.php)(\..*+))?$/i')
1594
            ->notName('/^conduct((?!\.php)(\..*+))?$/i')
1595
            ->notName('/^todo((?!\.php)(\..*+))?$/i')
1596
            ->exclude('doc')
1597
            ->exclude('docs')
1598
            ->exclude('documentation')
1599
            // Remove backup files
1600
            ->notName('*~')
1601
            ->notName('*.back')
1602
            ->notName('*.swp')
1603
            // Remove tests
1604
            ->notName('*Test.php')
1605
            ->exclude('test')
1606
            ->exclude('Test')
1607
            ->exclude('tests')
1608
            ->exclude('Tests')
1609
            ->notName('/phpunit.*\.xml(.dist)?/')
1610
            ->notName('/behat.*\.yml(.dist)?/')
1611
            ->exclude('spec')
1612
            ->exclude('specs')
1613
            ->exclude('features')
1614
            // Remove CI config
1615
            ->exclude('travis')
1616
            ->notName('travis.yml')
1617
            ->notName('appveyor.yml')
1618
            ->notName('build.xml*')
1619
        ;
1620
1621
        if (null !== $mainScriptPath) {
1622
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1623
        }
1624
1625
        $finder->in($directories);
1626
1627
        $excludedPaths = array_unique(
1628
            array_filter(
1629
                array_map(
1630
                    static function (string $path) use ($basePath): string {
1631
                        return make_path_relative($path, $basePath);
1632
                    },
1633
                    $excludedPaths
1634
                ),
1635
                static function (string $path): bool {
1636
                    return 0 !== strpos($path, '..');
1637
                }
1638
            )
1639
        );
1640
1641
        foreach ($excludedPaths as $excludedPath) {
1642
            $finder->notPath($excludedPath);
1643
        }
1644
1645
        return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[].
Loading history...
1646
    }
1647
1648
    /**
1649
     * @param string $key Config property name
1650
     *
1651
     * @return string[]
1652
     */
1653
    private static function retrieveDirectoryPaths(
1654
        stdClass $raw,
1655
        string $key,
1656
        string $basePath,
1657
        ConfigurationLogger $logger
1658
    ): array {
1659
        self::checkIfDefaultValue($logger, $raw, $key, []);
1660
1661
        if (false === isset($raw->{$key})) {
1662
            return [];
1663
        }
1664
1665
        $directories = $raw->{$key};
1666
1667
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1668
            $directory = self::normalizePath($directory, $basePath);
1669
1670
            Assertion::false(
1671
                is_link($directory),
1672
                sprintf(
1673
                    'Cannot add the link "%s": links are not supported.',
1674
                    $directory
1675
                )
1676
            );
1677
1678
            Assertion::directory(
1679
                $directory,
1680
                sprintf(
1681
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1682
                    $key
1683
                )
1684
            );
1685
1686
            return $directory;
1687
        };
1688
1689
        return array_map($normalizeDirectory, $directories);
1690
    }
1691
1692
    private static function normalizePath(string $file, string $basePath): string
1693
    {
1694
        return make_path_absolute(trim($file), $basePath);
1695
    }
1696
1697
    /**
1698
     * @param string[] $files
1699
     *
1700
     * @return SplFileInfo[]
1701
     */
1702
    private static function wrapInSplFileInfo(array $files): array
1703
    {
1704
        return array_map(
1705
            static function (string $file): SplFileInfo {
1706
                return new SplFileInfo($file);
1707
            },
1708
            $files
1709
        );
1710
    }
1711
1712
    private static function retrieveDumpAutoload(stdClass $raw, ComposerFiles $composerFiles, ConfigurationLogger $logger): bool
1713
    {
1714
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, null);
1715
1716
        $canDumpAutoload = (
1717
            null !== $composerFiles->getComposerJson()->getPath()
1718
            && (
1719
                // The composer.lock and installed.json are optional (e.g. if there is no dependencies installed)
1720
                // but when one is present, the other must be as well otherwise the dumped autoloader will be broken
1721
                (
1722
                    null === $composerFiles->getComposerLock()->getPath()
1723
                    && null === $composerFiles->getInstalledJson()->getPath()
1724
                )
1725
                || (
1726
                    null !== $composerFiles->getComposerLock()->getPath()
1727
                    && null !== $composerFiles->getInstalledJson()->getPath()
1728
                )
1729
                || (
1730
                    null === $composerFiles->getComposerLock()->getPath()
1731
                    && null !== $composerFiles->getInstalledJson()->getPath()
1732
                    && [] === $composerFiles->getInstalledJson()->getDecodedContents()
1733
                )
1734
            )
1735
        );
1736
1737
        if ($canDumpAutoload) {
1738
            self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1739
        }
1740
1741
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1742
            return $canDumpAutoload;
1743
        }
1744
1745
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1746
1747
        if (false === $canDumpAutoload && $dumpAutoload) {
1748
            $logger->addWarning(
1749
                sprintf(
1750
                    'The "%s" setting has been set but has been ignored because the composer.json, composer.lock'
1751
                    .' and vendor/composer/installed.json files are necessary but could not be found.',
1752
                    self::DUMP_AUTOLOAD_KEY
1753
                )
1754
            );
1755
1756
            return false;
1757
        }
1758
1759
        return $canDumpAutoload && false !== $dumpAutoload;
1760
    }
1761
1762
    private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool
1763
    {
1764
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload);
1765
1766
        if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) {
1767
            return $dumpAutoload;
1768
        }
1769
1770
        $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload;
1771
1772
        if (true === $excludeDevFiles && false === $dumpAutoload) {
1773
            $logger->addWarning(sprintf(
1774
                'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped',
1775
                self::EXCLUDE_DEV_FILES_KEY
1776
            ));
1777
1778
            return false;
1779
        }
1780
1781
        return $excludeDevFiles;
1782
    }
1783
1784
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1785
    {
1786
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1787
1788
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1789
    }
1790
1791
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactors
1792
    {
1793
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1794
1795
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1796
1797
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1798
            return new Compactors();
1799
        }
1800
1801
        $compactors = new Compactors(
1802
            self::createCompactors(
0 ignored issues
show
Bug introduced by
self::createCompactors($...ses, $logger), $logger) of type array is incompatible with the type KevinGH\Box\Compactor\Compactor expected by parameter $compactors of KevinGH\Box\Compactor\Compactors::__construct(). ( Ignorable by Annotation )

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

1802
            /** @scrutinizer ignore-type */ self::createCompactors(
Loading history...
1803
                $raw,
1804
                $basePath,
1805
                $compactorClasses,
1806
                self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger),
1807
                $logger
1808
            )
1809
        );
1810
1811
        self::checkCompactorsOrder($logger, $compactors);
1812
1813
        return $compactors;
1814
    }
1815
1816
    /**
1817
     * @param string[] $compactorClasses
1818
     * @param string[] $ignoredAnnotations
1819
     *
1820
     * @return Compactor[]
1821
     */
1822
    private static function createCompactors(
1823
        stdClass $raw,
1824
        string $basePath,
1825
        array $compactorClasses,
1826
        array $ignoredAnnotations,
1827
        ConfigurationLogger $logger
1828
    ): array {
1829
        return array_map(
1830
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1831
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1832
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1833
1834
                if (LegacyPhp::class === $class) {
1835
                    $logger->addRecommendation(
1836
                        sprintf(
1837
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1838
                            LegacyPhp::class,
1839
                            PhpCompactor::class
1840
                        )
1841
                    );
1842
                }
1843
1844
                if (LegacyJson::class === $class) {
1845
                    $logger->addRecommendation(
1846
                        sprintf(
1847
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1848
                            LegacyJson::class,
1849
                            JsonCompactor::class
1850
                        )
1851
                    );
1852
                }
1853
1854
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1855
                    return self::createPhpCompactor($ignoredAnnotations);
1856
                }
1857
1858
                if (PhpScoperCompactor::class === $class) {
1859
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1860
                }
1861
1862
                return static function () use ($class): Compactor {
0 ignored issues
show
Bug Best Practice introduced by
The expression return function(...) { /* ... */ } returns the type callable which is incompatible with the type-hinted return KevinGH\Box\Compactor\Compactor.
Loading history...
1863
                    return new $class();
1864
                };
1865
            },
1866
            $compactorClasses
1867
        );
1868
    }
1869
1870
    private static function checkCompactorsOrder(ConfigurationLogger $logger, Compactors $compactors): void
1871
    {
1872
        $scoperCompactor = false;
1873
1874
        foreach ($compactors->toArray() as $compactor) {
1875
            if ($compactor instanceof PhpScoperCompactor) {
1876
                $scoperCompactor = true;
1877
            }
1878
1879
            if ($compactor instanceof PhpCompactor) {
1880
                if (true === $scoperCompactor) {
1881
                    $logger->addRecommendation(
1882
                        sprintf(
1883
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1884
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1885
                        )
1886
                    );
1887
                }
1888
1889
                break;
1890
            }
1891
        }
1892
    }
1893
1894
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1895
    {
1896
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1897
1898
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1899
            return null;
1900
        }
1901
1902
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1903
1904
        Assertion::inArray(
1905
            $raw->{self::COMPRESSION_KEY},
1906
            $knownAlgorithmNames,
1907
            sprintf(
1908
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1909
                implode('", "', $knownAlgorithmNames)
1910
            )
1911
        );
1912
1913
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1914
1915
        // Phar::NONE is not valid for compressFiles()
1916
        if (Phar::NONE === $value) {
1917
            return null;
1918
        }
1919
1920
        return $value;
1921
    }
1922
1923
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1924
    {
1925
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1926
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1927
        }
1928
1929
        $defaultChmod = intval(0755, 8);
1930
1931
        if (isset($raw->{self::CHMOD_KEY})) {
1932
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1933
1934
            if ($defaultChmod === $chmod) {
1935
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1936
            }
1937
1938
            return $chmod;
1939
        }
1940
1941
        return $defaultChmod;
1942
    }
1943
1944
    private static function retrieveMainScriptPath(
1945
        stdClass $raw,
1946
        string $basePath,
1947
        ?array $decodedJsonContents,
1948
        ConfigurationLogger $logger
1949
    ): ?string {
1950
        $firstBin = false;
1951
1952
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1953
            /** @var false|string $firstBin */
1954
            $firstBin = current((array) $decodedJsonContents['bin']);
1955
1956
            if (false !== $firstBin) {
1957
                $firstBin = self::normalizePath($firstBin, $basePath);
1958
            }
1959
        }
1960
1961
        if (isset($raw->{self::MAIN_KEY})) {
1962
            $main = $raw->{self::MAIN_KEY};
1963
1964
            if (is_string($main)) {
1965
                $main = self::normalizePath($main, $basePath);
1966
1967
                if ($main === $firstBin) {
1968
                    $logger->addRecommendation(
1969
                        sprintf(
1970
                            'The "%s" setting can be omitted since is set to its default value',
1971
                            self::MAIN_KEY
1972
                        )
1973
                    );
1974
                }
1975
            }
1976
        } else {
1977
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1978
        }
1979
1980
        if (is_bool($main)) {
1981
            Assertion::false(
1982
                $main,
1983
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1984
            );
1985
1986
            return null;
1987
        }
1988
1989
        Assertion::file($main);
1990
1991
        return $main;
1992
    }
1993
1994
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1995
    {
1996
        if (null === $mainScriptPath) {
1997
            return null;
1998
        }
1999
2000
        $contents = file_contents($mainScriptPath);
2001
2002
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
2003
        // PHAR entry point file.
2004
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
2005
        // case the main script can be ignored or disabled.
2006
        return preg_replace('/^#!.*\s*/', '', $contents);
2007
    }
2008
2009
    private static function retrieveComposerFiles(string $basePath): ComposerFiles
2010
    {
2011
        $retrieveFileAndContents = static function (string $file): ?ComposerFile {
2012
            $json = new Json();
2013
2014
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
2015
                return ComposerFile::createEmpty();
2016
            }
2017
2018
            try {
2019
                $contents = (array) $json->decodeFile($file, true);
2020
            } catch (ParsingException $exception) {
2021
                throw new InvalidArgumentException(
2022
                    sprintf(
2023
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
2024
                        $file,
2025
                        $exception->getMessage()
2026
                    ),
2027
                    0,
2028
                    $exception
2029
                );
2030
            }
2031
2032
            return new ComposerFile($file, $contents);
2033
        };
2034
2035
        return new ComposerFiles(
2036
            $retrieveFileAndContents(canonicalize($basePath.'/composer.json')),
2037
            $retrieveFileAndContents(canonicalize($basePath.'/composer.lock')),
2038
            $retrieveFileAndContents(canonicalize($basePath.'/vendor/composer/installed.json'))
2039
        );
2040
    }
2041
2042
    /**
2043
     * @return string[][]
2044
     */
2045
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
2046
    {
2047
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
2048
2049
        if (false === isset($raw->{self::MAP_KEY})) {
2050
            return [];
2051
        }
2052
2053
        $map = [];
2054
2055
        foreach ((array) $raw->{self::MAP_KEY} as $item) {
2056
            $processed = [];
2057
2058
            foreach ($item as $match => $replace) {
2059
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
2060
            }
2061
2062
            if (isset($processed['_empty_'])) {
2063
                $processed[''] = $processed['_empty_'];
2064
2065
                unset($processed['_empty_']);
2066
            }
2067
2068
            $map[] = $processed;
2069
        }
2070
2071
        return $map;
2072
    }
2073
2074
    /**
2075
     * @return mixed
2076
     */
2077
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
2078
    {
2079
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
2080
2081
        if (false === isset($raw->{self::METADATA_KEY})) {
2082
            return null;
2083
        }
2084
2085
        $metadata = $raw->{self::METADATA_KEY};
2086
2087
        return is_object($metadata) ? (array) $metadata : $metadata;
2088
    }
2089
2090
    /**
2091
     * @return string[] The first element is the temporary output path and the second the final one
2092
     */
2093
    private static function retrieveOutputPath(
2094
        stdClass $raw,
2095
        string $basePath,
2096
        ?string $mainScriptPath,
2097
        ConfigurationLogger $logger
2098
    ): array {
2099
        $defaultPath = null;
2100
2101
        if (null !== $mainScriptPath
2102
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
2103
        ) {
2104
            $defaultPath = $matches['main'].'.phar';
2105
        }
2106
2107
        if (isset($raw->{self::OUTPUT_KEY})) {
2108
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
2109
2110
            if ($path === $defaultPath) {
2111
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
2112
            }
2113
        } elseif (null !== $defaultPath) {
2114
            $path = $defaultPath;
2115
        } else {
2116
            // Last resort, should not happen
2117
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
2118
        }
2119
2120
        $tmp = $real = $path;
2121
2122
        if ('.phar' !== substr($real, -5)) {
2123
            $tmp .= '.phar';
2124
        }
2125
2126
        return [$tmp, $real];
2127
    }
2128
2129
    private static function retrievePrivateKeyPath(
2130
        stdClass $raw,
2131
        string $basePath,
2132
        int $signingAlgorithm,
2133
        ConfigurationLogger $logger
2134
    ): ?string {
2135
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
2136
            if (null === $raw->{self::KEY_KEY}) {
2137
                $logger->addRecommendation(
2138
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
2139
                );
2140
            } else {
2141
                $logger->addWarning(
2142
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
2143
                );
2144
            }
2145
2146
            return null;
2147
        }
2148
2149
        if (!isset($raw->{self::KEY_KEY})) {
2150
            Assertion::true(
2151
                Phar::OPENSSL !== $signingAlgorithm,
2152
                'Expected to have a private key for OpenSSL signing but none have been provided.'
2153
            );
2154
2155
            return null;
2156
        }
2157
2158
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
2159
2160
        Assertion::file($path);
2161
2162
        return $path;
2163
    }
2164
2165
    private static function retrievePrivateKeyPassphrase(
2166
        stdClass $raw,
2167
        int $algorithm,
2168
        ConfigurationLogger $logger
2169
    ): ?string {
2170
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
2171
2172
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
2173
            return null;
2174
        }
2175
2176
        /** @var null|false|string $keyPass */
2177
        $keyPass = $raw->{self::KEY_PASS_KEY};
2178
2179
        if (Phar::OPENSSL !== $algorithm) {
2180
            if (false === $keyPass || null === $keyPass) {
2181
                $logger->addRecommendation(
2182
                    sprintf(
2183
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
2184
                        .'not "OPENSSL".',
2185
                        self::KEY_PASS_KEY
2186
                    )
2187
                );
2188
            } else {
2189
                $logger->addWarning(
2190
                    sprintf(
2191
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
2192
                        self::KEY_PASS_KEY
2193
                    )
2194
                );
2195
            }
2196
2197
            return null;
2198
        }
2199
2200
        return is_string($keyPass) ? $keyPass : null;
2201
    }
2202
2203
    /**
2204
     * @return scalar[]
2205
     */
2206
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
2207
    {
2208
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
2209
2210
        if (null === $file) {
2211
            return [];
2212
        }
2213
2214
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
2215
2216
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
2217
            $replacements[$git] = self::retrievePrettyGitTag($file);
2218
        }
2219
2220
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
2221
            $replacements[$git] = self::retrieveGitHash($file);
2222
        }
2223
2224
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
2225
            $replacements[$git] = self::retrieveGitHash($file, true);
2226
        }
2227
2228
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
2229
            $replacements[$git] = self::retrieveGitTag($file);
2230
        }
2231
2232
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
2233
            $replacements[$git] = self::retrieveGitVersion($file);
2234
        }
2235
2236
        /**
2237
         * @var string
2238
         * @var bool   $valueSetByUser
2239
         */
2240
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2241
2242
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2243
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2244
        } elseif ($valueSetByUser) {
2245
            $logger->addRecommendation(
2246
                sprintf(
2247
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2248
                    self::DATETIME_FORMAT_KEY,
2249
                    self::DATETIME_KEY
2250
                )
2251
            );
2252
        }
2253
2254
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2255
2256
        foreach ($replacements as $key => $value) {
2257
            unset($replacements[$key]);
2258
            $replacements[$sigil.$key.$sigil] = $value;
2259
        }
2260
2261
        return $replacements;
2262
    }
2263
2264
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2265
    {
2266
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2267
    }
2268
2269
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2270
    {
2271
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2272
    }
2273
2274
    /**
2275
     * @param bool $short Use the short version
2276
     *
2277
     * @return string the commit hash
2278
     */
2279
    private static function retrieveGitHash(string $file, bool $short = false): string
2280
    {
2281
        return self::runGitCommand(
2282
            sprintf(
2283
                'git log --pretty="%s" -n1 HEAD',
2284
                $short ? '%h' : '%H'
2285
            ),
2286
            $file
2287
        );
2288
    }
2289
2290
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2291
    {
2292
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2293
    }
2294
2295
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2296
    {
2297
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2298
    }
2299
2300
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2301
    {
2302
        self::checkIfDefaultValue($logger, $raw, $key);
2303
2304
        return $raw->{$key} ?? null;
2305
    }
2306
2307
    private static function retrieveGitTag(string $file): string
2308
    {
2309
        return self::runGitCommand('git describe --tags HEAD', $file);
2310
    }
2311
2312
    private static function retrievePrettyGitTag(string $file): string
2313
    {
2314
        $version = self::retrieveGitTag($file);
2315
2316
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2317
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2318
        }
2319
2320
        return $version;
2321
    }
2322
2323
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2324
    {
2325
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2326
    }
2327
2328
    private static function retrieveGitVersion(string $file): ?string
2329
    {
2330
        try {
2331
            return self::retrieveGitTag($file);
2332
        } catch (RuntimeException $exception) {
2333
            try {
2334
                return self::retrieveGitHash($file, true);
2335
            } catch (RuntimeException $exception) {
2336
                throw new RuntimeException(
2337
                    sprintf(
2338
                        'The tag or commit hash could not be retrieved from "%s": %s',
2339
                        dirname($file),
2340
                        $exception->getMessage()
2341
                    ),
2342
                    0,
2343
                    $exception
2344
                );
2345
            }
2346
        }
2347
    }
2348
2349
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2350
    {
2351
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2352
    }
2353
2354
    private static function retrieveDatetimeNow(string $format): string
2355
    {
2356
        return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format($format);
2357
    }
2358
2359
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2360
    {
2361
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2362
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2363
2364
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2365
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2366
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2367
            @trigger_error(
2368
                sprintf(
2369
                    'The "%s" is deprecated, use "%s" setting instead.',
2370
                    self::DATETIME_FORMAT_DEPRECATED_KEY,
2371
                    self::DATETIME_FORMAT_KEY
2372
                ),
2373
                E_USER_DEPRECATED
2374
            );
2375
            $logger->addWarning(
2376
                sprintf(
2377
                    'The "%s" is deprecated, use "%s" setting instead.',
2378
                    self::DATETIME_FORMAT_DEPRECATED_KEY,
2379
                    self::DATETIME_FORMAT_KEY
2380
                )
2381
            );
2382
2383
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2384
        } else {
2385
            $format = null;
2386
        }
2387
2388
        if (null !== $format) {
2389
            $formattedDate = (new DateTimeImmutable())->format($format);
2390
2391
            Assertion::false(
2392
                false === $formattedDate || $formattedDate === $format,
2393
                sprintf(
2394
                    'Expected the datetime format to be a valid format: "%s" is not',
2395
                    $format
2396
                )
2397
            );
2398
2399
            return [$format, true];
2400
        }
2401
2402
        return [self::DEFAULT_DATETIME_FORMAT, false];
2403
    }
2404
2405
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2406
    {
2407
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2408
    }
2409
2410
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2411
    {
2412
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2413
2414
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2415
            return self::DEFAULT_SHEBANG;
2416
        }
2417
2418
        $shebang = $raw->{self::SHEBANG_KEY};
2419
2420
        if (false === $shebang) {
2421
            if (false === $stubIsGenerated) {
2422
                $logger->addRecommendation(
2423
                    sprintf(
2424
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2425
                        .' being used',
2426
                        self::SHEBANG_KEY
2427
                    )
2428
                );
2429
            }
2430
2431
            return null;
2432
        }
2433
2434
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2435
2436
        $shebang = trim($shebang);
2437
2438
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2439
        Assertion::true(
2440
            0 === strpos($shebang, '#!'),
2441
            sprintf(
2442
                'The shebang line must start with "#!". Got "%s" instead',
2443
                $shebang
2444
            )
2445
        );
2446
2447
        if (false === $stubIsGenerated) {
2448
            $logger->addWarning(
2449
                sprintf(
2450
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2451
                    .' used',
2452
                    self::SHEBANG_KEY
2453
                )
2454
            );
2455
        }
2456
2457
        return $shebang;
2458
    }
2459
2460
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2461
    {
2462
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2463
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2464
        }
2465
2466
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2467
            return self::DEFAULT_SIGNING_ALGORITHM;
2468
        }
2469
2470
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2471
2472
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2473
2474
        Assertion::true(
2475
            defined('Phar::'.$algorithm),
2476
            sprintf(
2477
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2478
                $algorithm
2479
            )
2480
        );
2481
2482
        $algorithm = constant('Phar::'.$algorithm);
2483
2484
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2485
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2486
        }
2487
2488
        return $algorithm;
2489
    }
2490
2491
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2492
    {
2493
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner());
2494
2495
        if (false === isset($raw->{self::BANNER_KEY})) {
2496
            return self::getDefaultBanner();
2497
        }
2498
2499
        $banner = $raw->{self::BANNER_KEY};
2500
2501
        if (false === $banner) {
2502
            if (false === $stubIsGenerated) {
2503
                $logger->addRecommendation(
2504
                    sprintf(
2505
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2506
                        .'being used',
2507
                        self::BANNER_KEY
2508
                    )
2509
                );
2510
            }
2511
2512
            return null;
2513
        }
2514
2515
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2516
2517
        if (is_array($banner)) {
2518
            $banner = implode("\n", $banner);
2519
        }
2520
2521
        if (false === $stubIsGenerated) {
2522
            $logger->addWarning(
2523
                sprintf(
2524
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2525
                    self::BANNER_KEY
2526
                )
2527
            );
2528
        }
2529
2530
        return $banner;
2531
    }
2532
2533
    private static function getDefaultBanner(): string
2534
    {
2535
        return sprintf(self::DEFAULT_BANNER, get_box_version());
2536
    }
2537
2538
    private static function retrieveStubBannerPath(
2539
        stdClass $raw,
2540
        string $basePath,
2541
        bool $stubIsGenerated,
2542
        ConfigurationLogger $logger
2543
    ): ?string {
2544
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2545
2546
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2547
            return null;
2548
        }
2549
2550
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2551
2552
        Assertion::file($bannerFile);
2553
2554
        if (false === $stubIsGenerated) {
2555
            $logger->addWarning(
2556
                sprintf(
2557
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2558
                    self::BANNER_FILE_KEY
2559
                )
2560
            );
2561
        }
2562
2563
        return $bannerFile;
2564
    }
2565
2566
    private static function normalizeStubBannerContents(?string $contents): ?string
2567
    {
2568
        if (null === $contents) {
2569
            return null;
2570
        }
2571
2572
        $banner = explode("\n", $contents);
2573
        $banner = array_map('trim', $banner);
2574
2575
        return implode("\n", $banner);
2576
    }
2577
2578
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2579
    {
2580
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2581
2582
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2583
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2584
2585
            Assertion::file($stubPath);
2586
2587
            return $stubPath;
2588
        }
2589
2590
        return null;
2591
    }
2592
2593
    private static function retrieveInterceptsFileFunctions(
2594
        stdClass $raw,
2595
        bool $stubIsGenerated,
2596
        ConfigurationLogger $logger
2597
    ): bool {
2598
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2599
2600
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2601
            return false;
2602
        }
2603
2604
        $intercept = $raw->{self::INTERCEPT_KEY};
2605
2606
        if ($intercept && false === $stubIsGenerated) {
2607
            $logger->addWarning(
2608
                sprintf(
2609
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2610
                    self::INTERCEPT_KEY
2611
                )
2612
            );
2613
        }
2614
2615
        return $intercept;
2616
    }
2617
2618
    private static function retrievePromptForPrivateKey(
2619
        stdClass $raw,
2620
        int $signingAlgorithm,
2621
        ConfigurationLogger $logger
2622
    ): bool {
2623
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2624
            if (Phar::OPENSSL !== $signingAlgorithm) {
2625
                $logger->addWarning(
2626
                    'A prompt for password for the private key has been requested but ignored since the signing '
2627
                    .'algorithm used is not "OPENSSL.'
2628
                );
2629
2630
                return false;
2631
            }
2632
2633
            return true;
2634
        }
2635
2636
        return false;
2637
    }
2638
2639
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2640
    {
2641
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2642
2643
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2644
    }
2645
2646
    private static function retrieveCheckRequirements(
2647
        stdClass $raw,
2648
        bool $hasComposerJson,
2649
        bool $hasComposerLock,
2650
        bool $pharStubUsed,
2651
        ConfigurationLogger $logger
2652
    ): bool {
2653
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2654
2655
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2656
            return $hasComposerJson || $hasComposerLock;
2657
        }
2658
2659
        /** @var bool $checkRequirements */
2660
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2661
2662
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2663
            $logger->addWarning(
2664
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2665
                .'be found.'
2666
            );
2667
2668
            return false;
2669
        }
2670
2671
        if ($checkRequirements && $pharStubUsed) {
2672
            $logger->addWarning(
2673
                sprintf(
2674
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2675
                    .'used.',
2676
                    self::CHECK_REQUIREMENTS_KEY
2677
                )
2678
            );
2679
        }
2680
2681
        return $checkRequirements;
2682
    }
2683
2684
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2685
    {
2686
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2687
2688
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2689
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2690
2691
            return file_exists($configFilePath)
2692
                ? PhpScoperConfiguration::load($configFilePath)
2693
                : PhpScoperConfiguration::load()
2694
             ;
2695
        }
2696
2697
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2698
2699
        Assertion::string($configFile);
2700
2701
        $configFilePath = make_path_absolute($configFile, $basePath);
2702
2703
        Assertion::file($configFilePath);
2704
        Assertion::readable($configFilePath);
2705
2706
        return PhpScoperConfiguration::load($configFilePath);
2707
    }
2708
2709
    /**
2710
     * Runs a Git command on the repository.
2711
     *
2712
     * @return string The trimmed output from the command
2713
     */
2714
    private static function runGitCommand(string $command, string $file): string
2715
    {
2716
        $path = dirname($file);
2717
2718
        $process = Process::fromShellCommandline($command, $path);
2719
2720
        if (0 === $process->run()) {
2721
            return trim($process->getOutput());
2722
        }
2723
2724
        throw new RuntimeException(
2725
            sprintf(
2726
                'The tag or commit hash could not be retrieved from "%s": %s',
2727
                $path,
2728
                $process->getErrorOutput()
2729
            )
2730
        );
2731
    }
2732
2733
    /**
2734
     * @param string[] $compactorClasses
2735
     *
2736
     * @return string[]
2737
     */
2738
    private static function retrievePhpCompactorIgnoredAnnotations(
2739
        stdClass $raw,
2740
        array $compactorClasses,
2741
        ConfigurationLogger $logger
2742
    ): array {
2743
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true) || in_array(LegacyPhp::class, $compactorClasses, true);
2744
2745
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2746
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2747
2748
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2749
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2750
        }
2751
2752
        if (false === $hasPhpCompactor) {
2753
            $logger->addWarning(
2754
                sprintf(
2755
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2756
                    self::ANNOTATIONS_KEY
2757
                )
2758
            );
2759
        }
2760
2761
        /** @var null|bool|stdClass $annotations */
2762
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2763
2764
        if (true === $annotations || null === $annotations) {
2765
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2766
        }
2767
2768
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2769
            return [];
2770
        }
2771
2772
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2773
            $logger->addWarning(
2774
                sprintf(
2775
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2776
                    .' if it is set to `false`',
2777
                    self::ANNOTATIONS_KEY,
2778
                    self::IGNORED_ANNOTATIONS_KEY,
2779
                    self::ANNOTATIONS_KEY
2780
                )
2781
            );
2782
2783
            return [];
2784
        }
2785
2786
        $ignored = [];
2787
2788
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2789
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2790
        ) {
2791
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2792
2793
            return (array) $ignored;
2794
        }
2795
2796
        return $ignored;
2797
    }
2798
2799
    private static function createPhpCompactor(array $ignoredAnnotations): Compactor
2800
    {
2801
        $ignoredAnnotations = array_values(
2802
            array_filter(
2803
                array_map(
2804
                    static function (string $annotation): ?string {
2805
                        return strtolower(trim($annotation));
2806
                    },
2807
                    $ignoredAnnotations
2808
                )
2809
            )
2810
        );
2811
2812
        return new PhpCompactor(
2813
            new DocblockAnnotationParser(
2814
                new DocblockParser(),
2815
                new AnnotationDumper(),
2816
                $ignoredAnnotations
2817
            )
2818
        );
2819
    }
2820
2821
    private static function createPhpScoperCompactor(
2822
        stdClass $raw,
2823
        string $basePath,
2824
        ConfigurationLogger $logger
2825
    ): Compactor {
2826
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2827
2828
        $whitelistedFiles = array_values(
2829
            array_unique(
2830
                array_map(
2831
                    static function (string $path) use ($basePath): string {
2832
                        return make_path_relative($path, $basePath);
2833
                    },
2834
                    $phpScoperConfig->getWhitelistedFiles()
2835
                )
2836
            )
2837
        );
2838
2839
        $prefix = $phpScoperConfig->getPrefix() ?? unique_id('_HumbugBox');
2840
2841
        return static function () use ($phpScoperConfig, $prefix, $whitelistedFiles): Compactor {
0 ignored issues
show
Bug Best Practice introduced by
The expression return function(...) { /* ... */ } returns the type callable which is incompatible with the type-hinted return KevinGH\Box\Compactor\Compactor.
Loading history...
2842
            $phpScoper = (new class() extends ApplicationFactory {
2843
                public static function createScoper(): Scoper
2844
                {
2845
                    return parent::createScoper();
2846
                }
2847
            })::createScoper();
2848
2849
            if ([] !== $whitelistedFiles) {
2850
                $phpScoper = new FileWhitelistScoper($phpScoper, ...$whitelistedFiles);
2851
            }
2852
2853
            return new PhpScoperCompactor(
2854
                new SimpleScoper(
2855
                    $phpScoper,
2856
                    $prefix,
2857
                    $phpScoperConfig->getWhitelist(),
2858
                    $phpScoperConfig->getPatchers()
2859
                )
2860
            );
2861
        };
2862
    }
2863
2864
    private static function checkIfDefaultValue(
2865
        ConfigurationLogger $logger,
2866
        stdClass $raw,
2867
        string $key,
2868
        $defaultValue = null
2869
    ): void {
2870
        if (false === property_exists($raw, $key)) {
2871
            return;
2872
        }
2873
2874
        $value = $raw->{$key};
2875
2876
        if (null === $value
2877
            || (false === is_object($defaultValue) && $defaultValue === $value)
2878
            || (is_object($defaultValue) && $defaultValue == $value)
2879
        ) {
2880
            $logger->addRecommendation(
2881
                sprintf(
2882
                    'The "%s" setting can be omitted since is set to its default value',
2883
                    $key
2884
                )
2885
            );
2886
        }
2887
    }
2888
2889
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2890
    {
2891
        $logger->addRecommendation(
2892
            sprintf(
2893
                'The "%s" setting can be omitted since is set to its default value',
2894
                $key
2895
            )
2896
        );
2897
    }
2898
}
2899