Passed
Pull Request — master (#337)
by Théo
03:16 queued 43s
created

Configuration::collectFiles()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 87
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 87
rs 7.5789
c 0
b 0
f 0
cc 9
nc 20
nop 11

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Herrera\Box\Compactor\Json as LegacyJson;
22
use Herrera\Box\Compactor\Php as LegacyPhp;
23
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
24
use Humbug\PhpScoper\Console\ApplicationFactory;
25
use Humbug\PhpScoper\Scoper;
26
use Humbug\PhpScoper\Scoper\FileWhitelistScoper;
27
use InvalidArgumentException;
28
use KevinGH\Box\Annotation\AnnotationDumper;
29
use KevinGH\Box\Annotation\DocblockAnnotationParser;
30
use KevinGH\Box\Annotation\DocblockParser;
31
use KevinGH\Box\Compactor\Json as JsonCompactor;
32
use KevinGH\Box\Compactor\Php as PhpCompactor;
33
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
34
use KevinGH\Box\Composer\ComposerConfiguration;
35
use KevinGH\Box\Json\Json;
36
use KevinGH\Box\PhpScoper\SimpleScoper;
37
use Phar;
38
use RuntimeException;
39
use Seld\JsonLint\ParsingException;
40
use SplFileInfo;
41
use stdClass;
42
use Symfony\Component\Finder\Finder;
43
use Symfony\Component\Process\Process;
44
use const E_USER_DEPRECATED;
45
use function array_column;
46
use function array_diff;
47
use function array_filter;
48
use function array_flip;
49
use function array_key_exists;
50
use function array_keys;
51
use function array_map;
52
use function array_merge;
53
use function array_unique;
54
use function array_values;
55
use function array_walk;
56
use function constant;
57
use function current;
58
use function defined;
59
use function dirname;
60
use function explode;
61
use function file_exists;
62
use function getcwd;
63
use function implode;
64
use function in_array;
65
use function intval;
66
use function is_array;
67
use function is_bool;
68
use function is_file;
69
use function is_link;
70
use function is_object;
71
use function is_readable;
72
use function is_string;
73
use function iter\map;
74
use function iter\toArray;
75
use function iter\values;
76
use function KevinGH\Box\FileSystem\canonicalize;
77
use function KevinGH\Box\FileSystem\file_contents;
78
use function KevinGH\Box\FileSystem\is_absolute_path;
79
use function KevinGH\Box\FileSystem\longest_common_base_path;
80
use function KevinGH\Box\FileSystem\make_path_absolute;
81
use function KevinGH\Box\FileSystem\make_path_relative;
82
use function krsort;
83
use function preg_match;
84
use function preg_replace;
85
use function property_exists;
86
use function realpath;
87
use function sprintf;
88
use function strtoupper;
89
use function substr;
90
use function trigger_error;
91
use function trim;
92
93
/**
94
 * @private
95
 */
96
final class Configuration
97
{
98
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
99
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
100
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
101
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
102
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
103
    private const DEFAULT_BANNER = <<<'BANNER'
104
Generated by Humbug Box %s.
105
106
@link https://github.com/humbug/box
107
BANNER;
108
    private const FILES_SETTINGS = [
109
        'directories',
110
        'finder',
111
    ];
112
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
113
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
114
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
115
116
    private const DEFAULT_IGNORED_ANNOTATIONS = [
117
        'abstract',
118
        'access',
119
        'annotation',
120
        'api',
121
        'attribute',
122
        'attributes',
123
        'author',
124
        'category',
125
        'code',
126
        'codecoverageignore',
127
        'codecoverageignoreend',
128
        'codecoverageignorestart',
129
        'copyright',
130
        'deprec',
131
        'deprecated',
132
        'endcode',
133
        'example',
134
        'exception',
135
        'filesource',
136
        'final',
137
        'fixme',
138
        'global',
139
        'ignore',
140
        'ingroup',
141
        'inheritdoc',
142
        'internal',
143
        'license',
144
        'link',
145
        'magic',
146
        'method',
147
        'name',
148
        'override',
149
        'package',
150
        'package_version',
151
        'param',
152
        'private',
153
        'property',
154
        'required',
155
        'return',
156
        'see',
157
        'since',
158
        'static',
159
        'staticvar',
160
        'subpackage',
161
        'suppresswarnings',
162
        'target',
163
        'throw',
164
        'throws',
165
        'todo',
166
        'tutorial',
167
        'usedby',
168
        'uses',
169
        'var',
170
        'version',
171
    ];
172
173
    private const ALGORITHM_KEY = 'algorithm';
174
    private const ALIAS_KEY = 'alias';
175
    private const ANNOTATIONS_KEY = 'annotations';
176
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
177
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
178
    private const BANNER_KEY = 'banner';
179
    private const BANNER_FILE_KEY = 'banner-file';
180
    private const BASE_PATH_KEY = 'base-path';
181
    private const BLACKLIST_KEY = 'blacklist';
182
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
183
    private const CHMOD_KEY = 'chmod';
184
    private const COMPACTORS_KEY = 'compactors';
185
    private const COMPRESSION_KEY = 'compression';
186
    private const DATETIME_KEY = 'datetime';
187
    private const DATETIME_FORMAT_KEY = 'datetime-format';
188
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
189
    private const DIRECTORIES_KEY = 'directories';
190
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
191
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
192
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
193
    private const FILES_KEY = 'files';
194
    private const FILES_BIN_KEY = 'files-bin';
195
    private const FINDER_KEY = 'finder';
196
    private const FINDER_BIN_KEY = 'finder-bin';
197
    private const GIT_KEY = 'git';
198
    private const GIT_COMMIT_KEY = 'git-commit';
199
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
200
    private const GIT_TAG_KEY = 'git-tag';
201
    private const GIT_VERSION_KEY = 'git-version';
202
    private const INTERCEPT_KEY = 'intercept';
203
    private const KEY_KEY = 'key';
204
    private const KEY_PASS_KEY = 'key-pass';
205
    private const MAIN_KEY = 'main';
206
    private const MAP_KEY = 'map';
207
    private const METADATA_KEY = 'metadata';
208
    private const OUTPUT_KEY = 'output';
209
    private const PHP_SCOPER_KEY = 'php-scoper';
210
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
211
    private const REPLACEMENTS_KEY = 'replacements';
212
    private const SHEBANG_KEY = 'shebang';
213
    private const STUB_KEY = 'stub';
214
215
    private $file;
216
    private $fileMode;
217
    private $alias;
218
    private $basePath;
219
    private $composerJson;
220
    private $composerLock;
221
    private $files;
222
    private $binaryFiles;
223
    private $autodiscoveredFiles;
224
    private $dumpAutoload;
225
    private $excludeComposerFiles;
226
    private $compactors;
227
    private $compressionAlgorithm;
228
    private $mainScriptPath;
229
    private $mainScriptContents;
230
    private $fileMapper;
231
    private $metadata;
232
    private $tmpOutputPath;
233
    private $outputPath;
234
    private $privateKeyPassphrase;
235
    private $privateKeyPath;
236
    private $promptForPrivateKey;
237
    private $processedReplacements;
238
    private $shebang;
239
    private $signingAlgorithm;
240
    private $stubBannerContents;
241
    private $stubBannerPath;
242
    private $stubPath;
243
    private $isInterceptFileFuncs;
244
    private $isStubGenerated;
245
    private $checkRequirements;
246
    private $warnings;
247
    private $recommendations;
248
249
    public static function create(?string $file, stdClass $raw): self
250
    {
251
        $logger = new ConfigurationLogger();
252
253
        $basePath = self::retrieveBasePath($file, $raw, $logger);
254
255
        $composerFiles = self::retrieveComposerFiles($basePath);
256
257
        /**
258
         * @var (string|null)[]
259
         * @var (string|null)[] $composerLock
260
         * @var (string|null)[] $installedJson
261
         */
262
        [$composerJson, $composerLock, $installedJson] = $composerFiles;
263
264
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
265
266
        if ($dumpAutoload && null !== $installedJson[0] && null === $composerLock[0]) {
267
            $logger->addWarning(
268
                'A vendor/composer/installed.json file has been found but its related file composer.lock could not. '
269
                .'This is likely due to the file having been removed despite being necessary. This will not break the '
270
                .'build but the dump-autoload had to be disabled.'
271
            );
272
        }
273
274
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
275
276
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $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
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
302
303
        $checkRequirements = self::retrieveCheckRequirements(
304
            $raw,
305
            null !== $composerJson[0],
306
            null !== $composerLock[0],
307
            false === $isStubGenerated && null === $stubPath,
308
            $logger
309
        );
310
311
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
312
313
        /**
314
         * @var string[]
315
         * @var Closure  $blacklistFilter
316
         */
317
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
318
            $raw,
319
            $basePath,
320
            $logger,
321
            $tmpOutputPath,
322
            $outputPath,
323
            $mainScriptPath
324
        );
325
326
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
327
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
328
329
        $filesAggregate = self::collectFiles(
330
            $raw,
331
            $basePath,
332
            $mainScriptPath,
333
            $blacklistFilter,
334
            $excludedPaths,
335
            $devPackages,
336
            $composerFiles,
337
            $autodiscoverFiles,
338
            $forceFilesAutodiscovery,
339
            $dumpAutoload,
340
            $logger
341
        );
342
        $binaryFilesAggregate = self::collectBinaryFiles(
343
            $raw,
344
            $basePath,
345
            $mainScriptPath,
346
            $blacklistFilter,
347
            $excludedPaths,
348
            $devPackages,
349
            $logger
350
        );
351
352
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
353
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
354
355
        $fileMode = self::retrieveFileMode($raw, $logger);
356
357
        $map = self::retrieveMap($raw, $logger);
358
        $fileMapper = new MapFile($basePath, $map);
359
360
        $metadata = self::retrieveMetadata($raw, $logger);
361
362
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
363
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
364
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
365
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
366
367
        $replacements = self::retrieveReplacements($raw, $file, $logger);
368
369
        return new self(
370
            $file,
371
            $alias,
372
            $basePath,
373
            $composerJson,
374
            $composerLock,
375
            $filesAggregate,
376
            $binaryFilesAggregate,
377
            $autodiscoverFiles || $forceFilesAutodiscovery,
378
            $dumpAutoload,
379
            $excludeComposerFiles,
380
            $compactors,
381
            $compressionAlgorithm,
382
            $fileMode,
383
            $mainScriptPath,
384
            $mainScriptContents,
385
            $fileMapper,
386
            $metadata,
387
            $tmpOutputPath,
388
            $outputPath,
389
            $privateKeyPassphrase,
390
            $privateKeyPath,
391
            $promptForPrivateKey,
392
            $replacements,
393
            $shebang,
394
            $signingAlgorithm,
395
            $stubBannerContents,
396
            $stubBannerPath,
397
            $stubPath,
398
            $isInterceptsFileFuncs,
399
            $isStubGenerated,
400
            $checkRequirements,
401
            $logger->getWarnings(),
402
            $logger->getRecommendations()
403
        );
404
    }
405
406
    /**
407
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
408
     *                                            path relative to it (the base path)
409
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
410
     *                                            string and the second element its decoded contents as an
411
     *                                            associative array.
412
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
413
     *                                            string and the second element its decoded contents as an
414
     *                                            associative array.
415
     * @param SplFileInfo[] $files                List of files
416
     * @param SplFileInfo[] $binaryFiles          List of binary files
417
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
418
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
419
     *                                            installed.json should be removed from the PHAR
420
     * @param Compactor[]   $compactors           List of file contents compactors
421
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
422
     * @param null|int      $fileMode             File mode in octal form
423
     * @param string        $mainScriptPath       The main script file path
424
     * @param string        $mainScriptContents   The processed content of the main script file
425
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
426
     * @param mixed         $metadata             The PHAR Metadata
427
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
428
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
429
     * @param null|string   $shebang              The shebang line
430
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
431
     * @param null|string   $stubBannerContents   The stub banner comment
432
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
433
     * @param null|string   $stubPath             The PHAR stub file path
434
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
435
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
436
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
437
     *                                            running
438
     * @param string[]      $warnings
439
     * @param string[]      $recommendations
440
     */
441
    private function __construct(
442
        ?string $file,
443
        string $alias,
444
        string $basePath,
445
        array $composerJson,
446
        array $composerLock,
447
        array $files,
448
        array $binaryFiles,
449
        bool $autodiscoveredFiles,
450
        bool $dumpAutoload,
451
        bool $excludeComposerFiles,
452
        array $compactors,
453
        ?int $compressionAlgorithm,
454
        ?int $fileMode,
455
        ?string $mainScriptPath,
456
        ?string $mainScriptContents,
457
        MapFile $fileMapper,
458
        $metadata,
459
        string $tmpOutputPath,
460
        string $outputPath,
461
        ?string $privateKeyPassphrase,
462
        ?string $privateKeyPath,
463
        bool $promptForPrivateKey,
464
        array $replacements,
465
        ?string $shebang,
466
        int $signingAlgorithm,
467
        ?string $stubBannerContents,
468
        ?string $stubBannerPath,
469
        ?string $stubPath,
470
        bool $isInterceptFileFuncs,
471
        bool $isStubGenerated,
472
        bool $checkRequirements,
473
        array $warnings,
474
        array $recommendations
475
    ) {
476
        Assertion::nullOrInArray(
477
            $compressionAlgorithm,
478
            get_phar_compression_algorithms(),
479
            sprintf(
480
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
481
                implode('", "', array_keys(get_phar_compression_algorithms()))
482
            )
483
        );
484
485
        if (null === $mainScriptPath) {
486
            Assertion::null($mainScriptContents);
487
        } else {
488
            Assertion::notNull($mainScriptContents);
489
        }
490
491
        $this->file = $file;
492
        $this->alias = $alias;
493
        $this->basePath = $basePath;
494
        $this->composerJson = $composerJson;
495
        $this->composerLock = $composerLock;
496
        $this->files = $files;
497
        $this->binaryFiles = $binaryFiles;
498
        $this->autodiscoveredFiles = $autodiscoveredFiles;
499
        $this->dumpAutoload = $dumpAutoload;
500
        $this->excludeComposerFiles = $excludeComposerFiles;
501
        $this->compactors = $compactors;
502
        $this->compressionAlgorithm = $compressionAlgorithm;
503
        $this->fileMode = $fileMode;
504
        $this->mainScriptPath = $mainScriptPath;
505
        $this->mainScriptContents = $mainScriptContents;
506
        $this->fileMapper = $fileMapper;
507
        $this->metadata = $metadata;
508
        $this->tmpOutputPath = $tmpOutputPath;
509
        $this->outputPath = $outputPath;
510
        $this->privateKeyPassphrase = $privateKeyPassphrase;
511
        $this->privateKeyPath = $privateKeyPath;
512
        $this->promptForPrivateKey = $promptForPrivateKey;
513
        $this->processedReplacements = $replacements;
514
        $this->shebang = $shebang;
515
        $this->signingAlgorithm = $signingAlgorithm;
516
        $this->stubBannerContents = $stubBannerContents;
517
        $this->stubBannerPath = $stubBannerPath;
518
        $this->stubPath = $stubPath;
519
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
520
        $this->isStubGenerated = $isStubGenerated;
521
        $this->checkRequirements = $checkRequirements;
522
        $this->warnings = $warnings;
523
        $this->recommendations = $recommendations;
524
    }
525
526
    public function getConfigurationFile(): ?string
527
    {
528
        return $this->file;
529
    }
530
531
    public function getAlias(): string
532
    {
533
        return $this->alias;
534
    }
535
536
    public function getBasePath(): string
537
    {
538
        return $this->basePath;
539
    }
540
541
    public function getComposerJson(): ?string
542
    {
543
        return $this->composerJson[0];
544
    }
545
546
    public function getDecodedComposerJsonContents(): ?array
547
    {
548
        return $this->composerJson[1];
549
    }
550
551
    public function getComposerLock(): ?string
552
    {
553
        return $this->composerLock[0];
554
    }
555
556
    public function getDecodedComposerLockContents(): ?array
557
    {
558
        return $this->composerLock[1];
559
    }
560
561
    /**
562
     * @return SplFileInfo[]
563
     */
564
    public function getFiles(): array
565
    {
566
        return $this->files;
567
    }
568
569
    /**
570
     * @return SplFileInfo[]
571
     */
572
    public function getBinaryFiles(): array
573
    {
574
        return $this->binaryFiles;
575
    }
576
577
    public function hasAutodiscoveredFiles(): bool
578
    {
579
        return $this->autodiscoveredFiles;
580
    }
581
582
    public function dumpAutoload(): bool
583
    {
584
        return $this->dumpAutoload;
585
    }
586
587
    public function excludeComposerFiles(): bool
588
    {
589
        return $this->excludeComposerFiles;
590
    }
591
592
    /**
593
     * @return Compactor[] the list of compactors
594
     */
595
    public function getCompactors(): array
596
    {
597
        return $this->compactors;
598
    }
599
600
    public function getCompressionAlgorithm(): ?int
601
    {
602
        return $this->compressionAlgorithm;
603
    }
604
605
    public function getFileMode(): ?int
606
    {
607
        return $this->fileMode;
608
    }
609
610
    public function hasMainScript(): bool
611
    {
612
        return null !== $this->mainScriptPath;
613
    }
614
615
    public function getMainScriptPath(): string
616
    {
617
        Assertion::notNull(
618
            $this->mainScriptPath,
619
            'Cannot retrieve the main script path: no main script configured.'
620
        );
621
622
        return $this->mainScriptPath;
623
    }
624
625
    public function getMainScriptContents(): string
626
    {
627
        Assertion::notNull(
628
            $this->mainScriptPath,
629
            'Cannot retrieve the main script contents: no main script configured.'
630
        );
631
632
        return $this->mainScriptContents;
633
    }
634
635
    public function checkRequirements(): bool
636
    {
637
        return $this->checkRequirements;
638
    }
639
640
    public function getTmpOutputPath(): string
641
    {
642
        return $this->tmpOutputPath;
643
    }
644
645
    public function getOutputPath(): string
646
    {
647
        return $this->outputPath;
648
    }
649
650
    public function getFileMapper(): MapFile
651
    {
652
        return $this->fileMapper;
653
    }
654
655
    /**
656
     * @return mixed
657
     */
658
    public function getMetadata()
659
    {
660
        return $this->metadata;
661
    }
662
663
    public function getPrivateKeyPassphrase(): ?string
664
    {
665
        return $this->privateKeyPassphrase;
666
    }
667
668
    public function getPrivateKeyPath(): ?string
669
    {
670
        return $this->privateKeyPath;
671
    }
672
673
    /**
674
     * @deprecated Use promptForPrivateKey() instead
675
     */
676
    public function isPrivateKeyPrompt(): bool
677
    {
678
        return $this->promptForPrivateKey;
679
    }
680
681
    public function promptForPrivateKey(): bool
682
    {
683
        return $this->promptForPrivateKey;
684
    }
685
686
    /**
687
     * @return scalar[]
688
     */
689
    public function getReplacements(): array
690
    {
691
        return $this->processedReplacements;
692
    }
693
694
    public function getShebang(): ?string
695
    {
696
        return $this->shebang;
697
    }
698
699
    public function getSigningAlgorithm(): int
700
    {
701
        return $this->signingAlgorithm;
702
    }
703
704
    public function getStubBannerContents(): ?string
705
    {
706
        return $this->stubBannerContents;
707
    }
708
709
    public function getStubBannerPath(): ?string
710
    {
711
        return $this->stubBannerPath;
712
    }
713
714
    public function getStubPath(): ?string
715
    {
716
        return $this->stubPath;
717
    }
718
719
    public function isInterceptFileFuncs(): bool
720
    {
721
        return $this->isInterceptFileFuncs;
722
    }
723
724
    public function isStubGenerated(): bool
725
    {
726
        return $this->isStubGenerated;
727
    }
728
729
    /**
730
     * @return string[]
731
     */
732
    public function getWarnings(): array
733
    {
734
        return $this->warnings;
735
    }
736
737
    /**
738
     * @return string[]
739
     */
740
    public function getRecommendations(): array
741
    {
742
        return $this->recommendations;
743
    }
744
745
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
746
    {
747
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
748
749
        if (false === isset($raw->{self::ALIAS_KEY})) {
750
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
751
        }
752
753
        $alias = trim($raw->{self::ALIAS_KEY});
754
755
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
756
757
        if ($userStubUsed) {
758
            $logger->addWarning(
759
                sprintf(
760
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
761
                    self::ALIAS_KEY
762
                )
763
            );
764
        }
765
766
        return $alias;
767
    }
768
769
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
770
    {
771
        if (null === $file) {
772
            return getcwd();
773
        }
774
775
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
776
            return realpath(dirname($file));
777
        }
778
779
        $basePath = trim($raw->{self::BASE_PATH_KEY});
780
781
        Assertion::directory(
782
            $basePath,
783
            'The base path "%s" is not a directory or does not exist.'
784
        );
785
786
        $basePath = realpath($basePath);
787
        $defaultPath = realpath(dirname($file));
788
789
        if ($basePath === $defaultPath) {
790
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
791
        }
792
793
        return $basePath;
794
    }
795
796
    /**
797
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
798
     */
799
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
800
    {
801
        if (null === $file) {
802
            return true;
803
        }
804
805
        $associativeRaw = (array) $raw;
806
807
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
808
    }
809
810
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
811
    {
812
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
813
814
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
815
    }
816
817
    private static function retrieveBlacklistFilter(
818
        stdClass $raw,
819
        string $basePath,
820
        ConfigurationLogger $logger,
821
        ?string ...$excludedPaths
822
    ): array {
823
        $blacklist = array_flip(
824
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
825
        );
826
827
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
828
            if ($file->isLink()) {
829
                return false;
830
            }
831
832
            if (false === $file->getRealPath()) {
833
                return false;
834
            }
835
836
            if (array_key_exists($file->getRealPath(), $blacklist)) {
837
                return false;
838
            }
839
840
            return null;
841
        };
842
843
        return [array_keys($blacklist), $blacklistFilter];
844
    }
845
846
    /**
847
     * @param null[]|string[] $excludedPaths
848
     *
849
     * @return string[]
850
     */
851
    private static function retrieveBlacklist(
852
        stdClass $raw,
853
        string $basePath,
854
        ConfigurationLogger $logger,
855
        ?string ...$excludedPaths
856
    ): array {
857
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
858
859
        $normalizedBlacklist = array_map(
860
            static function (string $excludedPath) use ($basePath): string {
861
                return self::normalizePath($excludedPath, $basePath);
862
            },
863
            array_filter($excludedPaths)
864
        );
865
866
        /** @var string[] $blacklist */
867
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
868
869
        foreach ($blacklist as $file) {
870
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
871
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
872
        }
873
874
        return array_unique($normalizedBlacklist);
875
    }
876
877
    /**
878
     * @param string[] $excludedPaths
879
     * @param string[] $devPackages
880
     *
881
     * @return SplFileInfo[]
882
     */
883
    private static function collectFiles(
884
        stdClass $raw,
885
        string $basePath,
886
        ?string $mainScriptPath,
887
        Closure $blacklistFilter,
888
        array $excludedPaths,
889
        array $devPackages,
890
        array $composerFiles,
891
        bool $autodiscoverFiles,
892
        bool $forceFilesAutodiscovery,
893
        bool $dumpAutoload,
894
        ConfigurationLogger $logger
895
    ): array {
896
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $mainScriptPath, $logger)];
897
898
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
899
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
900
                $basePath,
901
                $composerFiles[0][1],
902
                $devPackages,
903
                array_filter(
904
                    array_column($composerFiles, 0)
905
                ),
906
                $excludedPaths
907
            );
908
909
            $files[] = self::wrapInSplFileInfo($filesToAppend);
910
911
            $files[] = self::retrieveAllFiles(
912
                $basePath,
913
                $directories,
914
                $mainScriptPath,
915
                $blacklistFilter,
916
                $excludedPaths,
917
                $devPackages
918
            );
919
        }
920
921
        if (false === $autodiscoverFiles) {
922
            $files[] = self::retrieveDirectories(
923
                $raw,
924
                self::DIRECTORIES_KEY,
925
                $basePath,
926
                $blacklistFilter,
927
                $excludedPaths,
928
                $logger
929
            );
930
931
            $filesFromFinders = self::retrieveFilesFromFinders(
932
                $raw,
933
                self::FINDER_KEY,
934
                $basePath,
935
                $blacklistFilter,
936
                $devPackages,
937
                $logger
938
            );
939
940
            foreach ($filesFromFinders as $filesFromFinder) {
941
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
942
                $files[] = $filesFromFinder;
943
            }
944
        }
945
946
        $aggregate = self::retrieveFilesAggregate(...$files);
947
948
        if (false === $dumpAutoload) {
949
            return $aggregate;
950
        }
951
952
        if (null === $composerFiles[1][0]) {
953
            return $aggregate;
954
        }
955
956
        foreach ($aggregate as $file) {
957
            if ($file->getRealPath() === $composerFiles[2][0]) {
958
                return $aggregate;
959
            }
960
        }
961
962
        $logger->addWarning(
963
            'A composer.lock file has been found but its related file vendor/composer/installed.json could not. This '
964
            .'could be due to either dependencies incorrectly installed or an incorrect Box configuration which is not '
965
            .'including the installed.json file. This will not break the build but will likely result in a broken '
966
            .'Composer classmap.'
967
        );
968
969
        return $aggregate;
970
    }
971
972
    /**
973
     * @param string[] $excludedPaths
974
     * @param string[] $devPackages
975
     *
976
     * @return SplFileInfo[]
977
     */
978
    private static function collectBinaryFiles(
979
        stdClass $raw,
980
        string $basePath,
981
        ?string $mainScriptPath,
982
        Closure $blacklistFilter,
983
        array $excludedPaths,
984
        array $devPackages,
985
        ConfigurationLogger $logger
986
    ): array {
987
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $mainScriptPath, $logger);
988
989
        $binaryDirectories = self::retrieveDirectories(
990
            $raw,
991
            self::DIRECTORIES_BIN_KEY,
992
            $basePath,
993
            $blacklistFilter,
994
            $excludedPaths,
995
            $logger
996
        );
997
998
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
999
            $raw,
1000
            self::FINDER_BIN_KEY,
1001
            $basePath,
1002
            $blacklistFilter,
1003
            $devPackages,
1004
            $logger
1005
        );
1006
1007
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
1008
    }
1009
1010
    /**
1011
     * @return SplFileInfo[]
1012
     */
1013
    private static function retrieveFiles(
1014
        stdClass $raw,
1015
        string $key,
1016
        string $basePath,
1017
        array $composerFiles,
1018
        ?string $mainScriptPath,
1019
        ConfigurationLogger $logger
1020
    ): array {
1021
        self::checkIfDefaultValue($logger, $raw, $key, []);
1022
1023
        $files = [];
1024
1025
        if (isset($composerFiles[0][0])) {
1026
            $files[] = $composerFiles[0][0];
1027
        }
1028
1029
        if (isset($composerFiles[1][1])) {
1030
            $files[] = $composerFiles[1][0];
1031
        }
1032
1033
        if (false === isset($raw->{$key})) {
1034
            return self::wrapInSplFileInfo($files);
1035
        }
1036
1037
        if ([] === (array) $raw->{$key}) {
1038
            return self::wrapInSplFileInfo($files);
1039
        }
1040
1041
        $files = array_merge((array) $raw->{$key}, $files);
1042
1043
        Assertion::allString($files);
1044
1045
        $normalizePath = static function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
1046
            $file = self::normalizePath($file, $basePath);
1047
1048
            Assertion::false(
1049
                is_link($file),
1050
                sprintf(
1051
                    'Cannot add the link "%s": links are not supported.',
1052
                    $file
1053
                )
1054
            );
1055
1056
            Assertion::file(
1057
                $file,
1058
                sprintf(
1059
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1060
                    $key
1061
                )
1062
            );
1063
1064
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
1065
        };
1066
1067
        return array_filter(array_map($normalizePath, $files));
1068
    }
1069
1070
    /**
1071
     * @param string   $key           Config property name
1072
     * @param string[] $excludedPaths
1073
     *
1074
     * @return iterable|SplFileInfo[]
1075
     */
1076
    private static function retrieveDirectories(
1077
        stdClass $raw,
1078
        string $key,
1079
        string $basePath,
1080
        Closure $blacklistFilter,
1081
        array $excludedPaths,
1082
        ConfigurationLogger $logger
1083
    ): iterable {
1084
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1085
1086
        if ([] !== $directories) {
1087
            $finder = Finder::create()
1088
                ->files()
1089
                ->filter($blacklistFilter)
1090
                ->ignoreVCS(true)
1091
                ->in($directories)
1092
            ;
1093
1094
            foreach ($excludedPaths as $excludedPath) {
1095
                $finder->notPath($excludedPath);
1096
            }
1097
1098
            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...
1099
        }
1100
1101
        return [];
1102
    }
1103
1104
    /**
1105
     * @param string[] $devPackages
1106
     *
1107
     * @return iterable[]|SplFileInfo[][]
1108
     */
1109
    private static function retrieveFilesFromFinders(
1110
        stdClass $raw,
1111
        string $key,
1112
        string $basePath,
1113
        Closure $blacklistFilter,
1114
        array $devPackages,
1115
        ConfigurationLogger $logger
1116
    ): array {
1117
        self::checkIfDefaultValue($logger, $raw, $key, []);
1118
1119
        if (false === isset($raw->{$key})) {
1120
            return [];
1121
        }
1122
1123
        $finder = $raw->{$key};
1124
1125
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1126
    }
1127
1128
    /**
1129
     * @param iterable[]|SplFileInfo[][] $fileIterators
1130
     *
1131
     * @return SplFileInfo[]
1132
     */
1133
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1134
    {
1135
        $files = [];
1136
1137
        foreach ($fileIterators as $fileIterator) {
1138
            foreach ($fileIterator as $file) {
1139
                $files[(string) $file] = $file;
1140
            }
1141
        }
1142
1143
        return array_values($files);
1144
    }
1145
1146
    /**
1147
     * @param string[] $devPackages
1148
     *
1149
     * @return Finder[]|SplFileInfo[][]
1150
     */
1151
    private static function processFinders(
1152
        array $findersConfig,
1153
        string $basePath,
1154
        Closure $blacklistFilter,
1155
        array $devPackages
1156
    ): array {
1157
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1158
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1159
        };
1160
1161
        return array_map($processFinderConfig, $findersConfig);
1162
    }
1163
1164
    /**
1165
     * @param string[] $devPackages
1166
     *
1167
     * @return Finder|SplFileInfo[]
1168
     */
1169
    private static function processFinder(
1170
        stdClass $config,
1171
        string $basePath,
1172
        Closure $blacklistFilter,
1173
        array $devPackages
1174
    ): Finder {
1175
        $finder = Finder::create()
1176
            ->files()
1177
            ->filter($blacklistFilter)
1178
            ->filter(
1179
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1180
                    foreach ($devPackages as $devPackage) {
1181
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1182
                            // File belongs to the dev package
1183
                            return false;
1184
                        }
1185
                    }
1186
1187
                    return true;
1188
                }
1189
            )
1190
            ->ignoreVCS(true)
1191
        ;
1192
1193
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1194
            $normalizedConfig = [];
1195
1196
            foreach ($config as $method => $arguments) {
1197
                $method = trim($method);
1198
                $arguments = (array) $arguments;
1199
1200
                Assertion::methodExists(
1201
                    $method,
1202
                    $finder,
1203
                    'The method "Finder::%s" does not exist.'
1204
                );
1205
1206
                $normalizedConfig[$method] = $arguments;
1207
            }
1208
1209
            krsort($normalizedConfig);
1210
1211
            return $normalizedConfig;
1212
        })((array) $config, $finder);
1213
1214
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1215
            $directory = self::normalizePath($directory, $basePath);
1216
1217
            Assertion::false(
1218
                is_link($directory),
1219
                sprintf(
1220
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1221
                    $directory
1222
                )
1223
            );
1224
1225
            Assertion::directory($directory);
1226
1227
            return $directory;
1228
        };
1229
1230
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1231
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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