Passed
Pull Request — master (#291)
by Théo
02:03
created

Configuration::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 83
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 43
dl 0
loc 83
rs 9.232
c 0
b 0
f 0
cc 2
nc 2
nop 33

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\Annotations\Tokenizer;
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 InvalidArgumentException;
27
use KevinGH\Box\Compactor\Php;
28
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
29
use KevinGH\Box\Composer\ComposerConfiguration;
30
use KevinGH\Box\Json\Json;
31
use KevinGH\Box\PhpScoper\SimpleScoper;
32
use Phar;
33
use RuntimeException;
34
use Seld\JsonLint\ParsingException;
35
use SplFileInfo;
36
use stdClass;
37
use Symfony\Component\Finder\Finder;
38
use Symfony\Component\Process\Process;
39
use const E_USER_DEPRECATED;
40
use function array_column;
41
use function array_diff;
42
use function array_filter;
43
use function array_key_exists;
44
use function array_keys;
45
use function array_map;
46
use function array_merge;
47
use function array_unique;
48
use function constant;
49
use function defined;
50
use function dirname;
51
use function file_exists;
52
use function in_array;
53
use function intval;
54
use function is_array;
55
use function is_bool;
56
use function is_file;
57
use function is_link;
58
use function is_object;
59
use function is_readable;
60
use function is_string;
61
use function iter\map;
62
use function iter\toArray;
63
use function iter\values;
64
use function KevinGH\Box\FileSystem\canonicalize;
65
use function KevinGH\Box\FileSystem\file_contents;
66
use function KevinGH\Box\FileSystem\is_absolute_path;
67
use function KevinGH\Box\FileSystem\longest_common_base_path;
68
use function KevinGH\Box\FileSystem\make_path_absolute;
69
use function KevinGH\Box\FileSystem\make_path_relative;
70
use function preg_match;
71
use function property_exists;
72
use function realpath;
73
use function sprintf;
74
use function strtoupper;
75
use function substr;
76
use function trigger_error;
77
78
/**
79
 * @private
80
 */
81
final class Configuration
82
{
83
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
84
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
85
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
86
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
87
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
88
    private const DEFAULT_BANNER = <<<'BANNER'
89
Generated by Humbug Box.
90
91
@link https://github.com/humbug/box
92
BANNER;
93
    private const FILES_SETTINGS = [
94
        'directories',
95
        'finder',
96
    ];
97
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
98
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
99
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
100
101
    private const ALGORITHM_KEY = 'algorithm';
102
    private const ALIAS_KEY = 'alias';
103
    private const ANNOTATIONS_KEY = 'annotations';
104
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
105
    private const BANNER_KEY = 'banner';
106
    private const BANNER_FILE_KEY = 'banner-file';
107
    private const BASE_PATH_KEY = 'base-path';
108
    private const BLACKLIST_KEY = 'blacklist';
109
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
110
    private const CHMOD_KEY = 'chmod';
111
    private const COMPACTORS_KEY = 'compactors';
112
    private const COMPRESSION_KEY = 'compression';
113
    private const DATETIME_KEY = 'datetime';
114
    private const DATETIME_FORMAT_KEY = 'datetime-format';
115
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
116
    private const DIRECTORIES_KEY = 'directories';
117
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
118
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
119
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
120
    private const FILES_KEY = 'files';
121
    private const FILES_BIN_KEY = 'files-bin';
122
    private const FINDER_KEY = 'finder';
123
    private const FINDER_BIN_KEY = 'finder-bin';
124
    private const GIT_KEY = 'git';
125
    private const GIT_COMMIT_KEY = 'git-commit';
126
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
127
    private const GIT_TAG_KEY = 'git-tag';
128
    private const GIT_VERSION_KEY = 'git-version';
129
    private const INTERCEPT_KEY = 'intercept';
130
    private const KEY_KEY = 'key';
131
    private const KEY_PASS_KEY = 'key-pass';
132
    private const MAIN_KEY = 'main';
133
    private const MAP_KEY = 'map';
134
    private const METADATA_KEY = 'metadata';
135
    private const OUTPUT_KEY = 'output';
136
    private const PHP_SCOPER_KEY = 'php-scoper';
137
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
138
    private const REPLACEMENTS_KEY = 'replacements';
139
    private const SHEBANG_KEY = 'shebang';
140
    private const STUB_KEY = 'stub';
141
142
    private $file;
143
    private $fileMode;
144
    private $alias;
145
    private $basePath;
146
    private $composerJson;
147
    private $composerLock;
148
    private $files;
149
    private $binaryFiles;
150
    private $autodiscoveredFiles;
151
    private $dumpAutoload;
152
    private $excludeComposerFiles;
153
    private $compactors;
154
    private $compressionAlgorithm;
155
    private $mainScriptPath;
156
    private $mainScriptContents;
157
    private $fileMapper;
158
    private $metadata;
159
    private $tmpOutputPath;
160
    private $outputPath;
161
    private $privateKeyPassphrase;
162
    private $privateKeyPath;
163
    private $promptForPrivateKey;
164
    private $processedReplacements;
165
    private $shebang;
166
    private $signingAlgorithm;
167
    private $stubBannerContents;
168
    private $stubBannerPath;
169
    private $stubPath;
170
    private $isInterceptFileFuncs;
171
    private $isStubGenerated;
172
    private $checkRequirements;
173
    private $warnings;
174
    private $recommendations;
175
176
    public static function create(?string $file, stdClass $raw): self
177
    {
178
        $logger = new ConfigurationLogger();
179
180
        $basePath = self::retrieveBasePath($file, $raw, $logger);
181
182
        $composerFiles = self::retrieveComposerFiles($basePath);
183
184
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
185
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
186
187
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
188
189
        /** @var (string|null)[] $composerJson */
190
        $composerJson = $composerFiles[0];
191
        /** @var (string|null)[] $composerJson */
192
        $composerLock = $composerFiles[1];
193
194
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
195
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
196
197
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
198
199
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
200
201
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
202
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
203
204
        if (null !== $stubBannerPath) {
205
            $stubBannerContents = file_contents($stubBannerPath);
206
        }
207
208
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
209
210
        if (null !== $stubBannerPath && self::DEFAULT_BANNER === $stubBannerContents) {
211
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
212
        }
213
214
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
215
216
        $checkRequirements = self::retrieveCheckRequirements(
217
            $raw,
218
            null !== $composerJson[0],
219
            null !== $composerLock[0],
220
            false === $isStubGenerated && null === $stubPath,
221
            $logger
222
        );
223
224
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
0 ignored issues
show
Bug introduced by
It seems like $composerJson[1] can also be of type string; however, parameter $composerJsonDecodedContents of KevinGH\Box\Composer\Com...::retrieveDevPackages() does only seem to accept null|array, 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

224
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, /** @scrutinizer ignore-type */ $composerJson[1], $composerLock[1]);
Loading history...
225
226
        /**
227
         * @var string[]
228
         * @var Closure  $blacklistFilter
229
         */
230
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
231
            $raw,
232
            $basePath,
233
            $logger,
234
            $tmpOutputPath,
235
            $outputPath,
236
            $mainScriptPath
237
        );
238
239
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
240
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
241
242
        $filesAggregate = self::collectFiles(
243
            $raw,
244
            $basePath,
245
            $mainScriptPath,
246
            $blacklistFilter,
247
            $excludedPaths,
248
            $devPackages,
249
            $composerFiles,
250
            $composerJson,
251
            $autodiscoverFiles,
252
            $forceFilesAutodiscovery,
253
            $logger
254
        );
255
        $binaryFilesAggregate = self::collectBinaryFiles(
256
            $raw,
257
            $basePath,
258
            $mainScriptPath,
259
            $blacklistFilter,
260
            $excludedPaths,
261
            $devPackages,
262
            $logger
263
        );
264
265
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
266
267
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
268
269
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
270
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
271
272
        $fileMode = self::retrieveFileMode($raw, $logger);
273
274
        $map = self::retrieveMap($raw, $logger);
275
        $fileMapper = new MapFile($basePath, $map);
276
277
        $metadata = self::retrieveMetadata($raw, $logger);
278
279
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
280
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
281
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
282
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
283
284
        $replacements = self::retrieveReplacements($raw, $file, $logger);
285
286
        return new self(
287
            $file,
288
            $alias,
289
            $basePath,
290
            $composerJson,
291
            $composerLock,
292
            $filesAggregate,
293
            $binaryFilesAggregate,
294
            $autodiscoverFiles || $forceFilesAutodiscovery,
295
            $dumpAutoload,
296
            $excludeComposerFiles,
297
            $compactors,
298
            $compressionAlgorithm,
299
            $fileMode,
300
            $mainScriptPath,
301
            $mainScriptContents,
302
            $fileMapper,
303
            $metadata,
304
            $tmpOutputPath,
305
            $outputPath,
306
            $privateKeyPassphrase,
307
            $privateKeyPath,
308
            $promptForPrivateKey,
309
            $replacements,
310
            $shebang,
311
            $signingAlgorithm,
312
            $stubBannerContents,
313
            $stubBannerPath,
314
            $stubPath,
315
            $isInterceptsFileFuncs,
316
            $isStubGenerated,
317
            $checkRequirements,
318
            $logger->getWarnings(),
319
            $logger->getRecommendations()
320
        );
321
    }
322
323
    /**
324
     * @param null|string   $file
325
     * @param null|string   $alias
326
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
327
     *                                            path relative to it (the base path)
328
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
329
     *                                            string and the second element its decoded contents as an
330
     *                                            associative array.
331
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
332
     *                                            string and the second element its decoded contents as an
333
     *                                            associative array.
334
     * @param SplFileInfo[] $files                List of files
335
     * @param SplFileInfo[] $binaryFiles          List of binary files
336
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
337
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
338
     *                                            installed.json should be removed from the PHAR
339
     * @param Compactor[]   $compactors           List of file contents compactors
340
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
341
     * @param null|int      $fileMode             File mode in octal form
342
     * @param string        $mainScriptPath       The main script file path
343
     * @param string        $mainScriptContents   The processed content of the main script file
344
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
345
     * @param mixed         $metadata             The PHAR Metadata
346
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
347
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
348
     * @param null|string   $shebang              The shebang line
349
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
350
     * @param null|string   $stubBannerContents   The stub banner comment
351
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
352
     * @param null|string   $stubPath             The PHAR stub file path
353
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
354
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
355
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
356
     *                                            running
357
     * @param string[]      $warnings
358
     * @param string[]      $recommendations
359
     */
360
    private function __construct(
361
        ?string $file,
362
        string $alias,
363
        string $basePath,
364
        array $composerJson,
365
        array $composerLock,
366
        array $files,
367
        array $binaryFiles,
368
        bool $autodiscoveredFiles,
369
        bool $dumpAutoload,
370
        bool $excludeComposerFiles,
371
        array $compactors,
372
        ?int $compressionAlgorithm,
373
        ?int $fileMode,
374
        ?string $mainScriptPath,
375
        ?string $mainScriptContents,
376
        MapFile $fileMapper,
377
        $metadata,
378
        string $tmpOutputPath,
379
        string $outputPath,
380
        ?string $privateKeyPassphrase,
381
        ?string $privateKeyPath,
382
        bool $promptForPrivateKey,
383
        array $replacements,
384
        ?string $shebang,
385
        int $signingAlgorithm,
386
        ?string $stubBannerContents,
387
        ?string $stubBannerPath,
388
        ?string $stubPath,
389
        bool $isInterceptFileFuncs,
390
        bool $isStubGenerated,
391
        bool $checkRequirements,
392
        array $warnings,
393
        array $recommendations
394
    ) {
395
        Assertion::nullOrInArray(
396
            $compressionAlgorithm,
397
            get_phar_compression_algorithms(),
398
            sprintf(
399
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
400
                implode('", "', array_keys(get_phar_compression_algorithms()))
401
            )
402
        );
403
404
        if (null === $mainScriptPath) {
405
            Assertion::null($mainScriptContents);
406
        } else {
407
            Assertion::notNull($mainScriptContents);
408
        }
409
410
        $this->file = $file;
411
        $this->alias = $alias;
412
        $this->basePath = $basePath;
413
        $this->composerJson = $composerJson;
414
        $this->composerLock = $composerLock;
415
        $this->files = $files;
416
        $this->binaryFiles = $binaryFiles;
417
        $this->autodiscoveredFiles = $autodiscoveredFiles;
418
        $this->dumpAutoload = $dumpAutoload;
419
        $this->excludeComposerFiles = $excludeComposerFiles;
420
        $this->compactors = $compactors;
421
        $this->compressionAlgorithm = $compressionAlgorithm;
422
        $this->fileMode = $fileMode;
423
        $this->mainScriptPath = $mainScriptPath;
424
        $this->mainScriptContents = $mainScriptContents;
425
        $this->fileMapper = $fileMapper;
426
        $this->metadata = $metadata;
427
        $this->tmpOutputPath = $tmpOutputPath;
428
        $this->outputPath = $outputPath;
429
        $this->privateKeyPassphrase = $privateKeyPassphrase;
430
        $this->privateKeyPath = $privateKeyPath;
431
        $this->promptForPrivateKey = $promptForPrivateKey;
432
        $this->processedReplacements = $replacements;
433
        $this->shebang = $shebang;
434
        $this->signingAlgorithm = $signingAlgorithm;
435
        $this->stubBannerContents = $stubBannerContents;
436
        $this->stubBannerPath = $stubBannerPath;
437
        $this->stubPath = $stubPath;
438
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
439
        $this->isStubGenerated = $isStubGenerated;
440
        $this->checkRequirements = $checkRequirements;
441
        $this->warnings = $warnings;
442
        $this->recommendations = $recommendations;
443
    }
444
445
    public function getConfigurationFile(): ?string
446
    {
447
        return $this->file;
448
    }
449
450
    public function getAlias(): string
451
    {
452
        return $this->alias;
453
    }
454
455
    public function getBasePath(): string
456
    {
457
        return $this->basePath;
458
    }
459
460
    public function getComposerJson(): ?string
461
    {
462
        return $this->composerJson[0];
463
    }
464
465
    public function getDecodedComposerJsonContents(): ?array
466
    {
467
        return $this->composerJson[1];
468
    }
469
470
    public function getComposerLock(): ?string
471
    {
472
        return $this->composerLock[0];
473
    }
474
475
    public function getDecodedComposerLockContents(): ?array
476
    {
477
        return $this->composerLock[1];
478
    }
479
480
    /**
481
     * @return string[]
482
     */
483
    public function getFiles(): array
484
    {
485
        return $this->files;
486
    }
487
488
    /**
489
     * @return string[]
490
     */
491
    public function getBinaryFiles(): array
492
    {
493
        return $this->binaryFiles;
494
    }
495
496
    public function hasAutodiscoveredFiles(): bool
497
    {
498
        return $this->autodiscoveredFiles;
499
    }
500
501
    public function dumpAutoload(): bool
502
    {
503
        return $this->dumpAutoload;
504
    }
505
506
    public function excludeComposerFiles(): bool
507
    {
508
        return $this->excludeComposerFiles;
509
    }
510
511
    /**
512
     * @return Compactor[] the list of compactors
513
     */
514
    public function getCompactors(): array
515
    {
516
        return $this->compactors;
517
    }
518
519
    public function getCompressionAlgorithm(): ?int
520
    {
521
        return $this->compressionAlgorithm;
522
    }
523
524
    public function getFileMode(): ?int
525
    {
526
        return $this->fileMode;
527
    }
528
529
    public function hasMainScript(): bool
530
    {
531
        return null !== $this->mainScriptPath;
532
    }
533
534
    public function getMainScriptPath(): string
535
    {
536
        Assertion::notNull(
537
            $this->mainScriptPath,
538
            'Cannot retrieve the main script path: no main script configured.'
539
        );
540
541
        return $this->mainScriptPath;
542
    }
543
544
    public function getMainScriptContents(): string
545
    {
546
        Assertion::notNull(
547
            $this->mainScriptPath,
548
            'Cannot retrieve the main script contents: no main script configured.'
549
        );
550
551
        return $this->mainScriptContents;
552
    }
553
554
    public function checkRequirements(): bool
555
    {
556
        return $this->checkRequirements;
557
    }
558
559
    public function getTmpOutputPath(): string
560
    {
561
        return $this->tmpOutputPath;
562
    }
563
564
    public function getOutputPath(): string
565
    {
566
        return $this->outputPath;
567
    }
568
569
    public function getFileMapper(): MapFile
570
    {
571
        return $this->fileMapper;
572
    }
573
574
    /**
575
     * @return mixed
576
     */
577
    public function getMetadata()
578
    {
579
        return $this->metadata;
580
    }
581
582
    public function getPrivateKeyPassphrase(): ?string
583
    {
584
        return $this->privateKeyPassphrase;
585
    }
586
587
    public function getPrivateKeyPath(): ?string
588
    {
589
        return $this->privateKeyPath;
590
    }
591
592
    /**
593
     * @deprecated Use promptForPrivateKey() instead
594
     */
595
    public function isPrivateKeyPrompt(): bool
596
    {
597
        return $this->promptForPrivateKey;
598
    }
599
600
    public function promptForPrivateKey(): bool
601
    {
602
        return $this->promptForPrivateKey;
603
    }
604
605
    /**
606
     * @return scalar[]
607
     */
608
    public function getReplacements(): array
609
    {
610
        return $this->processedReplacements;
611
    }
612
613
    public function getShebang(): ?string
614
    {
615
        return $this->shebang;
616
    }
617
618
    public function getSigningAlgorithm(): int
619
    {
620
        return $this->signingAlgorithm;
621
    }
622
623
    public function getStubBannerContents(): ?string
624
    {
625
        return $this->stubBannerContents;
626
    }
627
628
    public function getStubBannerPath(): ?string
629
    {
630
        return $this->stubBannerPath;
631
    }
632
633
    public function getStubPath(): ?string
634
    {
635
        return $this->stubPath;
636
    }
637
638
    public function isInterceptFileFuncs(): bool
639
    {
640
        return $this->isInterceptFileFuncs;
641
    }
642
643
    public function isStubGenerated(): bool
644
    {
645
        return $this->isStubGenerated;
646
    }
647
648
    /**
649
     * @return string[]
650
     */
651
    public function getWarnings(): array
652
    {
653
        return $this->warnings;
654
    }
655
656
    /**
657
     * @return string[]
658
     */
659
    public function getRecommendations(): array
660
    {
661
        return $this->recommendations;
662
    }
663
664
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
665
    {
666
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
667
668
        if (false === isset($raw->{self::ALIAS_KEY})) {
669
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
670
        }
671
672
        $alias = trim($raw->{self::ALIAS_KEY});
673
674
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
675
676
        if ($userStubUsed) {
677
            $logger->addWarning(
678
                sprintf(
679
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
680
                    self::ALIAS_KEY
681
                )
682
            );
683
        }
684
685
        return $alias;
686
    }
687
688
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
689
    {
690
        if (null === $file) {
691
            return getcwd();
692
        }
693
694
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
695
            return realpath(dirname($file));
696
        }
697
698
        $basePath = trim($raw->{self::BASE_PATH_KEY});
699
700
        Assertion::directory(
701
            $basePath,
702
            'The base path "%s" is not a directory or does not exist.'
703
        );
704
705
        $basePath = realpath($basePath);
706
        $defaultPath = realpath(dirname($file));
707
708
        if ($basePath === $defaultPath) {
709
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
710
        }
711
712
        return $basePath;
713
    }
714
715
    /**
716
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
717
     */
718
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
719
    {
720
        if (null === $file) {
721
            return true;
722
        }
723
724
        $associativeRaw = (array) $raw;
725
726
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
727
    }
728
729
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
730
    {
731
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
732
733
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
734
    }
735
736
    private static function retrieveBlacklistFilter(
737
        stdClass $raw,
738
        string $basePath,
739
        ConfigurationLogger $logger,
740
        ?string ...$excludedPaths
741
    ): array {
742
        $blacklist = self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths);
743
744
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
745
            if ($file->isLink()) {
746
                return false;
747
            }
748
749
            if (false === $file->getRealPath()) {
750
                return false;
751
            }
752
753
            if (in_array($file->getRealPath(), $blacklist, true)) {
754
                return false;
755
            }
756
757
            return null;
758
        };
759
760
        return [$blacklist, $blacklistFilter];
761
    }
762
763
    /**
764
     * @param stdClass        $raw
765
     * @param string          $basePath
766
     * @param null[]|string[] $excludedPaths
767
     *
768
     * @return string[]
769
     */
770
    private static function retrieveBlacklist(
771
        stdClass $raw,
772
        string $basePath,
773
        ConfigurationLogger $logger,
774
        ?string ...$excludedPaths
775
    ): array {
776
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
777
778
        /** @var string[] $blacklist */
779
        $blacklist = array_merge(
780
            array_filter($excludedPaths),
781
            $raw->{self::BLACKLIST_KEY} ?? []
782
        );
783
784
        $normalizedBlacklist = [];
785
786
        foreach ($blacklist as $file) {
787
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
788
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
789
        }
790
791
        return array_unique($normalizedBlacklist);
792
    }
793
794
    /**
795
     * @param string[] $excludedPaths
796
     * @param string[] $devPackages
797
     *
798
     * @return SplFileInfo[]
799
     */
800
    private static function collectFiles(
801
        stdClass $raw,
802
        string $basePath,
803
        ?string $mainScriptPath,
804
        Closure $blacklistFilter,
805
        array $excludedPaths,
806
        array $devPackages,
807
        array $composerFiles,
808
        array $composerJson,
809
        bool $autodiscoverFiles,
810
        bool $forceFilesAutodiscovery,
811
        ConfigurationLogger $logger
812
    ): array {
813
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $mainScriptPath, $logger)];
814
815
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
816
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
817
                $basePath,
818
                $composerJson[1],
819
                $devPackages,
820
                array_filter(
821
                    array_column($composerFiles, 0)
822
                ),
823
                $excludedPaths
824
            );
825
826
            $files[] = $filesToAppend;
827
828
            $files[] = self::retrieveAllFiles(
829
                $basePath,
830
                $directories,
831
                $mainScriptPath,
832
                $blacklistFilter,
833
                $excludedPaths,
834
                $devPackages
835
            );
836
        }
837
838
        if (false === $autodiscoverFiles) {
839
            $files[] = self::retrieveDirectories(
840
                $raw,
841
                self::DIRECTORIES_KEY,
842
                $basePath,
843
                $blacklistFilter,
844
                $excludedPaths,
845
                $logger
846
            );
847
848
            $filesFromFinders = self::retrieveFilesFromFinders(
849
                $raw,
850
                self::FINDER_KEY,
851
                $basePath,
852
                $blacklistFilter,
853
                $devPackages,
854
                $logger
855
            );
856
857
            foreach ($filesFromFinders as $filesFromFinder) {
858
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
859
                $files[] = $filesFromFinder;
860
            }
861
        }
862
863
        return self::retrieveFilesAggregate(...$files);
864
    }
865
866
    /**
867
     * @param string[] $excludedPaths
868
     * @param string[] $devPackages
869
     *
870
     * @return SplFileInfo[]
871
     */
872
    private static function collectBinaryFiles(
873
        stdClass $raw,
874
        string $basePath,
875
        ?string $mainScriptPath,
876
        Closure $blacklistFilter,
877
        array $excludedPaths,
878
        array $devPackages,
879
        ConfigurationLogger $logger
880
    ): array {
881
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $mainScriptPath, $logger);
882
883
        $binaryDirectories = self::retrieveDirectories(
884
            $raw,
885
            self::DIRECTORIES_BIN_KEY,
886
            $basePath,
887
            $blacklistFilter,
888
            $excludedPaths,
889
            $logger
890
        );
891
892
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
893
            $raw,
894
            self::FINDER_BIN_KEY,
895
            $basePath,
896
            $blacklistFilter,
897
            $devPackages,
898
            $logger
899
        );
900
901
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
902
    }
903
904
    /**
905
     * @return SplFileInfo[]
906
     */
907
    private static function retrieveFiles(
908
        stdClass $raw,
909
        string $key,
910
        string $basePath,
911
        array $composerFiles,
912
        ?string $mainScriptPath,
913
        ConfigurationLogger $logger
914
    ): array {
915
        self::checkIfDefaultValue($logger, $raw, $key, []);
916
917
        $files = [];
918
919
        if (isset($composerFiles[0][0])) {
920
            $files[] = $composerFiles[0][0];
921
        }
922
923
        if (isset($composerFiles[1][1])) {
924
            $files[] = $composerFiles[1][0];
925
        }
926
927
        if (false === isset($raw->{$key})) {
928
            return $files;
929
        }
930
931
        if ([] === (array) $raw->{$key}) {
932
            return $files;
933
        }
934
935
        $files = array_merge((array) $raw->{$key}, $files);
936
937
        Assertion::allString($files);
938
939
        $normalizePath = function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
940
            $file = self::normalizePath($file, $basePath);
941
942
            Assertion::false(
943
                is_link($file),
944
                sprintf(
945
                    'Cannot add the link "%s": links are not supported.',
946
                    $file
947
                )
948
            );
949
950
            Assertion::file(
951
                $file,
952
                sprintf(
953
                    '"%s" must contain a list of existing files. Could not find "%%s".',
954
                    $key
955
                )
956
            );
957
958
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
959
        };
960
961
        return array_filter(array_map($normalizePath, $files));
962
    }
963
964
    /**
965
     * @param string   $key           Config property name
966
     * @param string[] $excludedPaths
967
     *
968
     * @return iterable|SplFileInfo[]
969
     */
970
    private static function retrieveDirectories(
971
        stdClass $raw,
972
        string $key,
973
        string $basePath,
974
        Closure $blacklistFilter,
975
        array $excludedPaths,
976
        ConfigurationLogger $logger
977
    ): iterable {
978
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
979
980
        if ([] !== $directories) {
981
            $finder = Finder::create()
982
                ->files()
983
                ->filter($blacklistFilter)
984
                ->ignoreVCS(true)
985
                ->in($directories)
986
            ;
987
988
            foreach ($excludedPaths as $excludedPath) {
989
                $finder->notPath($excludedPath);
990
            }
991
992
            return $finder;
993
        }
994
995
        return [];
996
    }
997
998
    /**
999
     * @param string[] $devPackages
1000
     *
1001
     * @return iterable[]|SplFileInfo[][]
1002
     */
1003
    private static function retrieveFilesFromFinders(
1004
        stdClass $raw,
1005
        string $key,
1006
        string $basePath,
1007
        Closure $blacklistFilter,
1008
        array $devPackages,
1009
        ConfigurationLogger $logger
1010
    ): array {
1011
        self::checkIfDefaultValue($logger, $raw, $key, []);
1012
1013
        if (false === isset($raw->{$key})) {
1014
            return [];
1015
        }
1016
1017
        $finder = $raw->{$key};
1018
1019
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1020
    }
1021
1022
    /**
1023
     * @param iterable[]|SplFileInfo[][] $fileIterators
1024
     *
1025
     * @return SplFileInfo[]
1026
     */
1027
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1028
    {
1029
        $files = [];
1030
1031
        foreach ($fileIterators as $fileIterator) {
1032
            foreach ($fileIterator as $file) {
1033
                $files[(string) $file] = $file;
1034
            }
1035
        }
1036
1037
        return array_values($files);
1038
    }
1039
1040
    /**
1041
     * @param string[] $devPackages
1042
     *
1043
     * @return Finder[]|SplFileInfo[][]
1044
     */
1045
    private static function processFinders(
1046
        array $findersConfig,
1047
        string $basePath,
1048
        Closure $blacklistFilter,
1049
        array $devPackages
1050
    ): array {
1051
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1052
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1053
        };
1054
1055
        return array_map($processFinderConfig, $findersConfig);
1056
    }
1057
1058
    /**
1059
     * @param string[] $devPackages
1060
     *
1061
     * @return Finder|SplFileInfo[]
1062
     */
1063
    private static function processFinder(
1064
        stdClass $config,
1065
        string $basePath,
1066
        Closure $blacklistFilter,
1067
        array $devPackages
1068
    ): Finder {
1069
        $finder = Finder::create()
1070
            ->files()
1071
            ->filter($blacklistFilter)
1072
            ->filter(
1073
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
1074
                    foreach ($devPackages as $devPackage) {
1075
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1076
                            // File belongs to the dev package
1077
                            return false;
1078
                        }
1079
                    }
1080
1081
                    return true;
1082
                }
1083
            )
1084
            ->ignoreVCS(true)
1085
        ;
1086
1087
        $normalizedConfig = (function (array $config, Finder $finder): array {
1088
            $normalizedConfig = [];
1089
1090
            foreach ($config as $method => $arguments) {
1091
                $method = trim($method);
1092
                $arguments = (array) $arguments;
1093
1094
                Assertion::methodExists(
1095
                    $method,
1096
                    $finder,
1097
                    'The method "Finder::%s" does not exist.'
1098
                );
1099
1100
                $normalizedConfig[$method] = $arguments;
1101
            }
1102
1103
            krsort($normalizedConfig);
1104
1105
            return $normalizedConfig;
1106
        })((array) $config, $finder);
1107
1108
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
1109
            $directory = self::normalizePath($directory, $basePath);
1110
1111
            Assertion::false(
1112
                is_link($directory),
1113
                sprintf(
1114
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1115
                    $directory
1116
                )
1117
            );
1118
1119
            Assertion::directory($directory);
1120
1121
            return $directory;
1122
        };
1123
1124
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1125
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
1126
1127
            Assertion::false(
1128
                is_link($fileOrDirectory),
1129
                sprintf(
1130
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1131
                    $fileOrDirectory
1132
                )
1133
            );
1134
1135
            Assertion::true(
1136
                file_exists($fileOrDirectory),
1137
                sprintf(
1138
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1139
                    $fileOrDirectory
1140
                )
1141
            );
1142
1143
            if (false === is_file($fileOrDirectory)) {
1144
                Assertion::directory($fileOrDirectory);
1145
            } else {
1146
                Assertion::file($fileOrDirectory);
1147
            }
1148
1149
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1150
                $fileOrDirectory = null;
1151
            }
1152
        };
1153
1154
        foreach ($normalizedConfig as $method => $arguments) {
1155
            if ('in' === $method) {
1156
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1157
            }
1158
1159
            if ('exclude' === $method) {
1160
                $arguments = array_unique(array_map('trim', $arguments));
1161
            }
1162
1163
            if ('append' === $method) {
1164
                array_walk($arguments, $normalizeFileOrDirectory);
1165
1166
                $arguments = [array_filter($arguments)];
1167
            }
1168
1169
            foreach ($arguments as $argument) {
1170
                $finder->$method($argument);
1171
            }
1172
        }
1173
1174
        return $finder;
1175
    }
1176
1177
    /**
1178
     * @param string[] $devPackages
1179
     * @param string[] $filesToAppend
1180
     *
1181
     * @return string[][]
1182
     */
1183
    private static function retrieveAllDirectoriesToInclude(
1184
        string $basePath,
1185
        ?array $decodedJsonContents,
1186
        array $devPackages,
1187
        array $filesToAppend,
1188
        array $excludedPaths
1189
    ): array {
1190
        $toString = function ($file): string {
1191
            // @param string|SplFileInfo $file
1192
            return (string) $file;
1193
        };
1194
1195
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1196
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1197
        } else {
1198
            $vendorDir = self::normalizePath('vendor', $basePath);
1199
        }
1200
1201
        if (file_exists($vendorDir)) {
1202
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1203
            // dependencies are included in the `composer.json`
1204
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1205
1206
            if (file_exists($installedJsonFiles)) {
1207
                $filesToAppend[] = $installedJsonFiles;
1208
            }
1209
1210
            $vendorPackages = toArray(values(map(
1211
                $toString,
1212
                Finder::create()
1213
                    ->in($vendorDir)
1214
                    ->directories()
1215
                    ->depth(1)
1216
                    ->ignoreUnreadableDirs()
1217
                    ->filter(
1218
                        function (SplFileInfo $fileInfo): ?bool {
1219
                            if ($fileInfo->isLink()) {
1220
                                return false;
1221
                            }
1222
1223
                            return null;
1224
                        }
1225
                    )
1226
            )));
1227
1228
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1229
1230
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1231
                $files = toArray(values(map(
1232
                    $toString,
1233
                    Finder::create()
1234
                        ->in($basePath)
1235
                        ->files()
1236
                        ->depth(0)
1237
                )));
1238
1239
                $directories = toArray(values(map(
1240
                    $toString,
1241
                    Finder::create()
1242
                        ->in($basePath)
1243
                        ->notPath('vendor')
1244
                        ->directories()
1245
                        ->depth(0)
1246
                )));
1247
1248
                return [
1249
                    array_merge($files, $filesToAppend),
1250
                    array_merge($directories, $vendorPackages),
1251
                ];
1252
            }
1253
1254
            $paths = $vendorPackages;
1255
        } else {
1256
            $paths = [];
1257
        }
1258
1259
        $autoload = $decodedJsonContents['autoload'] ?? [];
1260
1261
        if (array_key_exists('psr-4', $autoload)) {
1262
            foreach ($autoload['psr-4'] as $path) {
1263
                /** @var string|string[] $path */
1264
                $composerPaths = (array) $path;
1265
1266
                foreach ($composerPaths as $composerPath) {
1267
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1268
                }
1269
            }
1270
        }
1271
1272
        if (array_key_exists('psr-0', $autoload)) {
1273
            foreach ($autoload['psr-0'] as $path) {
1274
                /** @var string|string[] $path */
1275
                $composerPaths = (array) $path;
1276
1277
                foreach ($composerPaths as $composerPath) {
1278
                    if ('' !== trim($composerPath)) {
1279
                        $paths[] = $composerPath;
1280
                    }
1281
                }
1282
            }
1283
        }
1284
1285
        if (array_key_exists('classmap', $autoload)) {
1286
            foreach ($autoload['classmap'] as $path) {
1287
                // @var string $path
1288
                $paths[] = $path;
1289
            }
1290
        }
1291
1292
        $normalizePath = function (string $path) use ($basePath): string {
1293
            return is_absolute_path($path)
1294
                ? canonicalize($path)
1295
                : self::normalizePath(trim($path, '/ '), $basePath)
1296
            ;
1297
        };
1298
1299
        if (array_key_exists('files', $autoload)) {
1300
            foreach ($autoload['files'] as $path) {
1301
                // @var string $path
1302
                $path = $normalizePath($path);
1303
1304
                Assertion::file($path);
1305
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1306
1307
                $filesToAppend[] = $path;
1308
            }
1309
        }
1310
1311
        $files = $filesToAppend;
1312
        $directories = [];
1313
1314
        foreach ($paths as $path) {
1315
            $path = $normalizePath($path);
1316
1317
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1318
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1319
1320
            if (is_file($path)) {
1321
                $files[] = $path;
1322
            } else {
1323
                $directories[] = $path;
1324
            }
1325
        }
1326
1327
        [$files, $directories] = [
1328
            array_unique($files),
1329
            array_unique($directories),
1330
        ];
1331
1332
        return [
1333
            array_diff($files, $excludedPaths),
1334
            array_diff($directories, $excludedPaths),
1335
        ];
1336
    }
1337
1338
    /**
1339
     * @param string[] $files
1340
     * @param string[] $directories
1341
     * @param string[] $excludedPaths
1342
     * @param string[] $devPackages
1343
     *
1344
     * @return SplFileInfo[]
1345
     */
1346
    private static function retrieveAllFiles(
1347
        string $basePath,
1348
        array $directories,
1349
        ?string $mainScriptPath,
1350
        Closure $blacklistFilter,
1351
        array $excludedPaths,
1352
        array $devPackages
1353
    ): iterable {
1354
        if ([] === $directories) {
1355
            return [];
1356
        }
1357
1358
        $relativeDevPackages = array_map(
1359
            function (string $packagePath) use ($basePath): string {
1360
                return make_path_relative($packagePath, $basePath);
1361
            },
1362
            $devPackages
1363
        );
1364
1365
        $finder = Finder::create()
1366
            ->files()
1367
            ->filter($blacklistFilter)
1368
            ->exclude($relativeDevPackages)
1369
            ->ignoreVCS(true)
1370
            ->ignoreDotFiles(true)
1371
            // Remove build files
1372
            ->notName('composer.json')
1373
            ->notName('composer.lock')
1374
            ->notName('Makefile')
1375
            ->notName('Vagrantfile')
1376
            ->notName('phpstan*.neon*')
1377
            ->notName('infection*.json*')
1378
            ->notName('humbug*.json*')
1379
            ->notName('easy-coding-standard.neon*')
1380
            ->notName('phpbench.json*')
1381
            ->notName('phpcs.xml*')
1382
            ->notName('psalm.xml*')
1383
            ->notName('scoper.inc*')
1384
            ->notName('box*.json*')
1385
            ->notName('phpdoc*.xml*')
1386
            ->notName('codecov.yml*')
1387
            ->notName('Dockerfile')
1388
            ->exclude('build')
1389
            ->exclude('dist')
1390
            ->exclude('example')
1391
            ->exclude('examples')
1392
            // Remove documentation
1393
            ->notName('*.md')
1394
            ->notName('*.rst')
1395
            ->notName('/^readme(\..*+)?$/i')
1396
            ->notName('/^upgrade(\..*+)?$/i')
1397
            ->notName('/^contributing(\..*+)?$/i')
1398
            ->notName('/^changelog(\..*+)?$/i')
1399
            ->notName('/^authors?(\..*+)?$/i')
1400
            ->notName('/^conduct(\..*+)?$/i')
1401
            ->notName('/^todo(\..*+)?$/i')
1402
            ->exclude('doc')
1403
            ->exclude('docs')
1404
            ->exclude('documentation')
1405
            // Remove backup files
1406
            ->notName('*~')
1407
            ->notName('*.back')
1408
            ->notName('*.swp')
1409
            // Remove tests
1410
            ->notName('*Test.php')
1411
            ->exclude('test')
1412
            ->exclude('Test')
1413
            ->exclude('tests')
1414
            ->exclude('Tests')
1415
            ->notName('/phpunit.*\.xml(.dist)?/')
1416
            ->notName('/behat.*\.yml(.dist)?/')
1417
            ->exclude('spec')
1418
            ->exclude('specs')
1419
            ->exclude('features')
1420
            // Remove CI config
1421
            ->exclude('travis')
1422
            ->notName('travis.yml')
1423
            ->notName('appveyor.yml')
1424
            ->notName('build.xml*')
1425
        ;
1426
1427
        if (null !== $mainScriptPath) {
1428
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1429
        }
1430
1431
        $finder->in($directories);
1432
1433
        $excludedPaths = array_unique(
1434
            array_filter(
1435
                array_map(
1436
                    function (string $path) use ($basePath): string {
1437
                        return make_path_relative($path, $basePath);
1438
                    },
1439
                    $excludedPaths
1440
                ),
1441
                function (string $path): bool {
1442
                    return '..' !== substr($path, 0, 2);
1443
                }
1444
            )
1445
        );
1446
1447
        foreach ($excludedPaths as $excludedPath) {
1448
            $finder->notPath($excludedPath);
1449
        }
1450
1451
        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...
1452
    }
1453
1454
    /**
1455
     * @param stdClass $raw
1456
     * @param string   $key      Config property name
1457
     * @param string   $basePath
1458
     *
1459
     * @return string[]
1460
     */
1461
    private static function retrieveDirectoryPaths(
1462
        stdClass $raw,
1463
        string $key,
1464
        string $basePath,
1465
        ConfigurationLogger $logger
1466
    ): array {
1467
        self::checkIfDefaultValue($logger, $raw, $key, []);
1468
1469
        if (false === isset($raw->{$key})) {
1470
            return [];
1471
        }
1472
1473
        $directories = $raw->{$key};
1474
1475
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1476
            $directory = self::normalizePath($directory, $basePath);
1477
1478
            Assertion::false(
1479
                is_link($directory),
1480
                sprintf(
1481
                    'Cannot add the link "%s": links are not supported.',
1482
                    $directory
1483
                )
1484
            );
1485
1486
            Assertion::directory(
1487
                $directory,
1488
                sprintf(
1489
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1490
                    $key
1491
                )
1492
            );
1493
1494
            return $directory;
1495
        };
1496
1497
        return array_map($normalizeDirectory, $directories);
1498
    }
1499
1500
    private static function normalizePath(string $file, string $basePath): string
1501
    {
1502
        return make_path_absolute(trim($file), $basePath);
1503
    }
1504
1505
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1506
    {
1507
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1508
1509
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1510
            return $composerJson;
1511
        }
1512
1513
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1514
1515
        if (false === $composerJson && $dumpAutoload) {
1516
            $logger->addWarning(
1517
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1518
                .' for it could not be found'
1519
            );
1520
1521
            return false;
1522
        }
1523
1524
        return $composerJson && false !== $dumpAutoload;
1525
    }
1526
1527
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1528
    {
1529
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1530
1531
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1532
    }
1533
1534
    /**
1535
     * @return Compactor[]
1536
     */
1537
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1538
    {
1539
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1540
1541
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1542
            return [];
1543
        }
1544
1545
        $compactorClasses = array_unique((array) $raw->{self::COMPACTORS_KEY});
1546
1547
        return array_map(
1548
            function (string $class) use ($raw, $basePath, $logger): Compactor {
1549
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1550
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1551
1552
                if (Php::class === $class || LegacyPhp::class === $class) {
1553
                    return self::createPhpCompactor($raw);
1554
                }
1555
1556
                if (PhpScoperCompactor::class === $class) {
1557
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
1558
1559
                    $prefix = null === $phpScoperConfig->getPrefix()
1560
                        ? unique_id('_HumbugBox')
1561
                        : $phpScoperConfig->getPrefix()
1562
                    ;
1563
1564
                    return new PhpScoperCompactor(
1565
                        new SimpleScoper(
1566
                            (new class() extends ApplicationFactory {
1567
                                public static function createScoper(): Scoper
1568
                                {
1569
                                    return parent::createScoper();
1570
                                }
1571
                            })::createScoper(),
1572
                            $prefix,
1573
                            $phpScoperConfig->getWhitelist(),
1574
                            $phpScoperConfig->getPatchers()
1575
                        )
1576
                    );
1577
                }
1578
1579
                return new $class();
1580
            },
1581
            $compactorClasses
1582
        );
1583
    }
1584
1585
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1586
    {
1587
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY);
1588
1589
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1590
            return null;
1591
        }
1592
1593
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1594
1595
        Assertion::inArray(
1596
            $raw->{self::COMPRESSION_KEY},
1597
            $knownAlgorithmNames,
1598
            sprintf(
1599
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1600
                implode('", "', $knownAlgorithmNames)
1601
            )
1602
        );
1603
1604
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1605
1606
        // Phar::NONE is not valid for compressFiles()
1607
        if (Phar::NONE === $value) {
1608
            return null;
1609
        }
1610
1611
        return $value;
1612
    }
1613
1614
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1615
    {
1616
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1617
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1618
        }
1619
1620
        $defaultChmod = intval(0755, 8);
1621
1622
        if (isset($raw->{self::CHMOD_KEY})) {
1623
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1624
1625
            if ($defaultChmod === $chmod) {
1626
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1627
            }
1628
1629
            return $chmod;
1630
        }
1631
1632
        return $defaultChmod;
1633
    }
1634
1635
    private static function retrieveMainScriptPath(
1636
        stdClass $raw,
1637
        string $basePath,
1638
        ?array $decodedJsonContents,
1639
        ConfigurationLogger $logger
1640
    ): ?string {
1641
        $firstBin = false;
1642
1643
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1644
            /** @var false|string $firstBin */
1645
            $firstBin = current((array) $decodedJsonContents['bin']);
1646
1647
            if (false !== $firstBin) {
1648
                $firstBin = self::normalizePath($firstBin, $basePath);
1649
            }
1650
        }
1651
1652
        if (isset($raw->{self::MAIN_KEY})) {
1653
            $main = $raw->{self::MAIN_KEY};
1654
1655
            if (is_string($main)) {
1656
                $main = self::normalizePath($main, $basePath);
1657
1658
                if ($main === $firstBin) {
1659
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1660
                }
1661
            }
1662
        } else {
1663
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1664
        }
1665
1666
        if (is_bool($main)) {
1667
            Assertion::false(
1668
                $main,
1669
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1670
            );
1671
1672
            return null;
1673
        }
1674
1675
        Assertion::file($main);
1676
1677
        return $main;
1678
    }
1679
1680
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1681
    {
1682
        if (null === $mainScriptPath) {
1683
            return null;
1684
        }
1685
1686
        $contents = file_contents($mainScriptPath);
1687
1688
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1689
        // PHAR entry point file.
1690
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1691
        // case the main script can be ignored or disabled.
1692
        return preg_replace('/^#!.*\s*/', '', $contents);
1693
    }
1694
1695
    /**
1696
     * @return string|null[][]
1697
     */
1698
    private static function retrieveComposerFiles(string $basePath): array
1699
    {
1700
        $retrieveFileAndContents = function (string $file): array {
1701
            $json = new Json();
1702
1703
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1704
                return [null, null];
1705
            }
1706
1707
            try {
1708
                $contents = $json->decodeFile($file, true);
1709
            } catch (ParsingException $exception) {
1710
                throw new InvalidArgumentException(
1711
                    sprintf(
1712
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1713
                        $file,
1714
                        $exception->getMessage()
1715
                    ),
1716
                    0,
1717
                    $exception
1718
                );
1719
            }
1720
1721
            return [$file, $contents];
1722
        };
1723
1724
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1725
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1726
1727
        return [
1728
            [$composerJson, $composerJsonContents],
1729
            [$composerLock, $composerLockContents],
1730
        ];
1731
    }
1732
1733
    /**
1734
     * @return string[][]
1735
     */
1736
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1737
    {
1738
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1739
1740
        if (false === isset($raw->{self::MAP_KEY})) {
1741
            return [];
1742
        }
1743
1744
        $map = [];
1745
        $rawMap = (array) $raw->{self::MAP_KEY};
1746
1747
        foreach ($rawMap as $item) {
1748
            $processed = [];
1749
1750
            foreach ($item as $match => $replace) {
1751
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1752
            }
1753
1754
            if (isset($processed['_empty_'])) {
1755
                $processed[''] = $processed['_empty_'];
1756
1757
                unset($processed['_empty_']);
1758
            }
1759
1760
            $map[] = $processed;
1761
        }
1762
1763
        return $map;
1764
    }
1765
1766
    /**
1767
     * @return mixed
1768
     */
1769
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1770
    {
1771
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1772
1773
        if (false === isset($raw->{self::METADATA_KEY})) {
1774
            return null;
1775
        }
1776
1777
        $metadata = $raw->{self::METADATA_KEY};
1778
1779
        return is_object($metadata) ? (array) $metadata : $metadata;
1780
    }
1781
1782
    /**
1783
     * @return string[] The first element is the temporary output path and the second the final one
1784
     */
1785
    private static function retrieveOutputPath(
1786
        stdClass $raw,
1787
        string $basePath,
1788
        ?string $mainScriptPath,
1789
        ConfigurationLogger $logger
1790
    ): array {
1791
        $defaultPath = null;
1792
1793
        if (null !== $mainScriptPath
1794
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)
1795
        ) {
1796
            $defaultPath = $matches['main'].'.phar';
1797
        }
1798
1799
        if (isset($raw->{self::OUTPUT_KEY})) {
1800
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1801
1802
            if ($path === $defaultPath) {
1803
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1804
            }
1805
        } elseif (null !== $defaultPath) {
1806
            $path = $defaultPath;
1807
        } else {
1808
            // Last resort, should not happen
1809
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1810
        }
1811
1812
        $tmp = $real = $path;
1813
1814
        if ('.phar' !== substr($real, -5)) {
1815
            $tmp .= '.phar';
1816
        }
1817
1818
        return [$tmp, $real];
1819
    }
1820
1821
    private static function retrievePrivateKeyPath(
1822
        stdClass $raw,
1823
        string $basePath,
1824
        int $signingAlgorithm,
1825
        ConfigurationLogger $logger
1826
    ): ?string {
1827
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1828
            if (null === $raw->{self::KEY_KEY}) {
1829
                $logger->addRecommendation(
1830
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1831
                );
1832
            } else {
1833
                $logger->addWarning(
1834
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1835
                );
1836
            }
1837
1838
            return null;
1839
        }
1840
1841
        if (!isset($raw->{self::KEY_KEY})) {
1842
            Assertion::true(
1843
                Phar::OPENSSL !== $signingAlgorithm,
1844
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1845
            );
1846
1847
            return null;
1848
        }
1849
1850
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1851
1852
        Assertion::file($path);
1853
1854
        return $path;
1855
    }
1856
1857
    private static function retrievePrivateKeyPassphrase(
1858
        stdClass $raw,
1859
        int $algorithm,
1860
        ConfigurationLogger $logger
1861
    ): ?string {
1862
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1863
1864
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1865
            return null;
1866
        }
1867
1868
        /** @var null|false|string $keyPass */
1869
        $keyPass = $raw->{self::KEY_PASS_KEY};
1870
1871
        if (Phar::OPENSSL !== $algorithm) {
1872
            if (false === $keyPass || null === $keyPass) {
1873
                $logger->addRecommendation(
1874
                    sprintf(
1875
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1876
                        .'not "OPENSSL".',
1877
                        self::KEY_PASS_KEY
1878
                    )
1879
                );
1880
            } else {
1881
                $logger->addWarning(
1882
                    sprintf(
1883
                    'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1884
                        self::KEY_PASS_KEY
1885
                    )
1886
                );
1887
            }
1888
1889
            return null;
1890
        }
1891
1892
        return is_string($keyPass) ? $keyPass : null;
1893
    }
1894
1895
    /**
1896
     * @return scalar[]
1897
     */
1898
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1899
    {
1900
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1901
1902
        if (null === $file) {
1903
            return [];
1904
        }
1905
1906
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1907
1908
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1909
            $replacements[$git] = self::retrievePrettyGitTag($file);
1910
        }
1911
1912
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1913
            $replacements[$git] = self::retrieveGitHash($file);
1914
        }
1915
1916
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
1917
            $replacements[$git] = self::retrieveGitHash($file, true);
1918
        }
1919
1920
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
1921
            $replacements[$git] = self::retrieveGitTag($file);
1922
        }
1923
1924
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
1925
            $replacements[$git] = self::retrieveGitVersion($file);
1926
        }
1927
1928
        /**
1929
         * @var string
1930
         * @var bool   $valueSetByUser
1931
         */
1932
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
1933
1934
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
1935
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
1936
        } elseif ($valueSetByUser) {
1937
            $logger->addRecommendation(
1938
                sprintf(
1939
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
1940
                    self::DATETIME_FORMAT_KEY,
1941
                    self::DATETIME_KEY
1942
                )
1943
            );
1944
        }
1945
1946
        $sigil = self::retrieveReplacementSigil($raw, $logger);
1947
1948
        foreach ($replacements as $key => $value) {
1949
            unset($replacements[$key]);
1950
            $replacements[$sigil.$key.$sigil] = $value;
1951
        }
1952
1953
        return $replacements;
1954
    }
1955
1956
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1957
    {
1958
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
1959
    }
1960
1961
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1962
    {
1963
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
1964
    }
1965
1966
    /**
1967
     * @param string $file
1968
     * @param bool   $short Use the short version
1969
     *
1970
     * @return string the commit hash
1971
     */
1972
    private static function retrieveGitHash(string $file, bool $short = false): string
1973
    {
1974
        return self::runGitCommand(
1975
            sprintf(
1976
                'git log --pretty="%s" -n1 HEAD',
1977
                $short ? '%h' : '%H'
1978
            ),
1979
            $file
1980
        );
1981
    }
1982
1983
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1984
    {
1985
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
1986
    }
1987
1988
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1989
    {
1990
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
1991
    }
1992
1993
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
1994
    {
1995
        self::checkIfDefaultValue($logger, $raw, $key);
1996
1997
        return $raw->{$key} ?? null;
1998
    }
1999
2000
    private static function retrieveGitTag(string $file): string
2001
    {
2002
        return self::runGitCommand('git describe --tags HEAD', $file);
2003
    }
2004
2005
    private static function retrievePrettyGitTag(string $file): string
2006
    {
2007
        $version = self::retrieveGitTag($file);
2008
2009
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2010
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2011
        }
2012
2013
        return $version;
2014
    }
2015
2016
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2017
    {
2018
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2019
    }
2020
2021
    private static function retrieveGitVersion(string $file): ?string
2022
    {
2023
        try {
2024
            return self::retrieveGitTag($file);
2025
        } catch (RuntimeException $exception) {
2026
            try {
2027
                return self::retrieveGitHash($file, true);
2028
            } catch (RuntimeException $exception) {
2029
                throw new RuntimeException(
2030
                    sprintf(
2031
                        'The tag or commit hash could not be retrieved from "%s": %s',
2032
                        dirname($file),
2033
                        $exception->getMessage()
2034
                    ),
2035
                    0,
2036
                    $exception
2037
                );
2038
            }
2039
        }
2040
    }
2041
2042
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2043
    {
2044
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2045
    }
2046
2047
    private static function retrieveDatetimeNow(string $format): string
2048
    {
2049
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2050
2051
        return $now->format($format);
2052
    }
2053
2054
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2055
    {
2056
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2057
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2058
2059
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2060
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2061
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2062
            @trigger_error(
2063
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2064
                E_USER_DEPRECATED
2065
            );
2066
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2067
2068
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2069
        } else {
2070
            $format = null;
2071
        }
2072
2073
        if (null !== $format) {
2074
            $formattedDate = (new DateTimeImmutable())->format($format);
2075
2076
            Assertion::false(
2077
                false === $formattedDate || $formattedDate === $format,
2078
                sprintf(
2079
                    'Expected the datetime format to be a valid format: "%s" is not',
2080
                    $format
2081
                )
2082
            );
2083
2084
            return [$format, true];
2085
        }
2086
2087
        return [self::DEFAULT_DATETIME_FORMAT, false];
2088
    }
2089
2090
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2091
    {
2092
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2093
    }
2094
2095
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2096
    {
2097
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2098
2099
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2100
            return self::DEFAULT_SHEBANG;
2101
        }
2102
2103
        $shebang = $raw->{self::SHEBANG_KEY};
2104
2105
        if (false === $shebang) {
2106
            if (false === $stubIsGenerated) {
2107
                $logger->addRecommendation(
2108
                    sprintf(
2109
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2110
                        .' being used',
2111
                        self::SHEBANG_KEY
2112
                    )
2113
                );
2114
            }
2115
2116
            return null;
2117
        }
2118
2119
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2120
2121
        $shebang = trim($shebang);
2122
2123
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2124
        Assertion::true(
2125
            '#!' === substr($shebang, 0, 2),
2126
            sprintf(
2127
                'The shebang line must start with "#!". Got "%s" instead',
2128
                $shebang
2129
            )
2130
        );
2131
2132
        if (false === $stubIsGenerated) {
2133
            $logger->addWarning(
2134
                sprintf(
2135
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2136
                    .' used',
2137
                    self::SHEBANG_KEY
2138
                )
2139
            );
2140
        }
2141
2142
        return $shebang;
2143
    }
2144
2145
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2146
    {
2147
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2148
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2149
        }
2150
2151
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2152
            return self::DEFAULT_SIGNING_ALGORITHM;
2153
        }
2154
2155
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2156
2157
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2158
2159
        Assertion::true(
2160
            defined('Phar::'.$algorithm),
2161
            sprintf(
2162
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2163
                $algorithm
2164
            )
2165
        );
2166
2167
        $algorithm = constant('Phar::'.$algorithm);
2168
2169
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2170
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2171
        }
2172
2173
        return $algorithm;
2174
    }
2175
2176
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2177
    {
2178
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::DEFAULT_BANNER);
2179
2180
        if (false === isset($raw->{self::BANNER_KEY})) {
2181
            return self::DEFAULT_BANNER;
2182
        }
2183
2184
        $banner = $raw->{self::BANNER_KEY};
2185
2186
        if (false === $banner) {
2187
            if (false === $stubIsGenerated) {
2188
                $logger->addRecommendation(
2189
                    sprintf(
2190
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2191
                        .'being used',
2192
                        self::BANNER_KEY
2193
                    )
2194
                );
2195
            }
2196
2197
            return null;
2198
        }
2199
2200
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2201
2202
        if (is_array($banner)) {
2203
            $banner = implode("\n", $banner);
2204
        }
2205
2206
        if (false === $stubIsGenerated) {
2207
            $logger->addWarning(
2208
                sprintf(
2209
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2210
                    self::BANNER_KEY
2211
                )
2212
            );
2213
        }
2214
2215
        return $banner;
2216
    }
2217
2218
    private static function retrieveStubBannerPath(
2219
        stdClass $raw,
2220
        string $basePath,
2221
        bool $stubIsGenerated,
2222
        ConfigurationLogger $logger
2223
    ): ?string {
2224
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2225
2226
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2227
            return null;
2228
        }
2229
2230
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2231
2232
        Assertion::file($bannerFile);
2233
2234
        if (false === $stubIsGenerated) {
2235
            $logger->addWarning(
2236
                sprintf(
2237
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2238
                    self::BANNER_FILE_KEY
2239
                )
2240
            );
2241
        }
2242
2243
        return $bannerFile;
2244
    }
2245
2246
    private static function normalizeStubBannerContents(?string $contents): ?string
2247
    {
2248
        if (null === $contents) {
2249
            return null;
2250
        }
2251
2252
        $banner = explode("\n", $contents);
2253
        $banner = array_map('trim', $banner);
2254
2255
        return implode("\n", $banner);
2256
    }
2257
2258
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2259
    {
2260
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2261
2262
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2263
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2264
2265
            Assertion::file($stubPath);
2266
2267
            return $stubPath;
2268
        }
2269
2270
        return null;
2271
    }
2272
2273
    private static function retrieveInterceptsFileFuncs(
2274
        stdClass $raw,
2275
        bool $stubIsGenerated,
2276
        ConfigurationLogger $logger
2277
    ): bool {
2278
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2279
2280
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2281
            return false;
2282
        }
2283
2284
        $intercept = $raw->{self::INTERCEPT_KEY};
2285
2286
        if ($intercept && false === $stubIsGenerated) {
2287
            $logger->addWarning(
2288
                sprintf(
2289
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2290
                    self::INTERCEPT_KEY
2291
                )
2292
            );
2293
        }
2294
2295
        return $intercept;
2296
    }
2297
2298
    private static function retrievePromptForPrivateKey(
2299
        stdClass $raw,
2300
        int $signingAlgorithm,
2301
        ConfigurationLogger $logger
2302
    ): bool {
2303
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2304
            if (Phar::OPENSSL !== $signingAlgorithm) {
2305
                $logger->addWarning(
2306
                    'A prompt for password for the private key has been requested but ignored since the signing '
2307
                    .'algorithm used is not "OPENSSL.'
2308
                );
2309
2310
                return false;
2311
            }
2312
2313
            return true;
2314
        }
2315
2316
        return false;
2317
    }
2318
2319
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2320
    {
2321
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2322
2323
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2324
    }
2325
2326
    private static function retrieveCheckRequirements(
2327
        stdClass $raw,
2328
        bool $hasComposerJson,
2329
        bool $hasComposerLock,
2330
        bool $pharStubUsed,
2331
        ConfigurationLogger $logger
2332
    ): bool {
2333
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2334
2335
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2336
            return $hasComposerJson || $hasComposerLock;
2337
        }
2338
2339
        /** @var bool $checkRequirements */
2340
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2341
2342
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2343
            $logger->addWarning(
2344
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2345
                .'be found.'
2346
            );
2347
2348
            return false;
2349
        }
2350
2351
        if ($checkRequirements && $pharStubUsed) {
2352
            $logger->addWarning(
2353
                sprintf(
2354
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2355
                    .'used.',
2356
                    self::CHECK_REQUIREMENTS_KEY
2357
                )
2358
            );
2359
        }
2360
2361
        return $checkRequirements;
2362
    }
2363
2364
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2365
    {
2366
        // TODO: add recommendations regarding the order
2367
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2368
2369
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2370
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2371
2372
            return file_exists($configFilePath)
2373
                ? PhpScoperConfiguration::load($configFilePath)
2374
                : PhpScoperConfiguration::load()
2375
             ;
2376
        }
2377
2378
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2379
2380
        Assertion::string($configFile);
2381
2382
        $configFilePath = make_path_absolute($configFile, $basePath);
2383
2384
        Assertion::file($configFilePath);
2385
        Assertion::readable($configFilePath);
2386
2387
        return PhpScoperConfiguration::load($configFilePath);
2388
    }
2389
2390
    /**
2391
     * Runs a Git command on the repository.
2392
     *
2393
     * @param string $command the command
2394
     *
2395
     * @return string the trimmed output from the command
2396
     */
2397
    private static function runGitCommand(string $command, string $file): string
2398
    {
2399
        $path = dirname($file);
2400
2401
        $process = new Process($command, $path);
2402
2403
        if (0 === $process->run()) {
2404
            return trim($process->getOutput());
2405
        }
2406
2407
        throw new RuntimeException(
2408
            sprintf(
2409
                'The tag or commit hash could not be retrieved from "%s": %s',
2410
                $path,
2411
                $process->getErrorOutput()
2412
            )
2413
        );
2414
    }
2415
2416
    private static function createPhpCompactor(stdClass $raw): Compactor
2417
    {
2418
        // TODO: false === not set; check & add test/doc
2419
        $tokenizer = new Tokenizer();
2420
2421
        if (false === empty($raw->{self::ANNOTATIONS_KEY}) && isset($raw->{self::ANNOTATIONS_KEY}->ignore)) {
2422
            $tokenizer->ignore(
2423
                (array) $raw->{self::ANNOTATIONS_KEY}->ignore
2424
            );
2425
        }
2426
2427
        return new Php($tokenizer);
2428
    }
2429
2430
    private static function checkIfDefaultValue(
2431
        ConfigurationLogger $logger,
2432
        stdClass $raw,
2433
        string $key,
2434
        $defaultValue = null
2435
    ): void {
2436
        if (false === property_exists($raw, $key)) {
2437
            return;
2438
        }
2439
2440
        $value = $raw->{$key};
2441
2442
        if (null === $value
2443
            || (false === is_object($defaultValue) && $defaultValue === $value)
2444
            || (is_object($defaultValue) && $defaultValue == $value)
2445
        ) {
2446
            $logger->addRecommendation(
2447
                sprintf(
2448
                    'The "%s" setting can be omitted since is set to its default value',
2449
                    $key
2450
                )
2451
            );
2452
        }
2453
    }
2454
2455
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2456
    {
2457
        $logger->addRecommendation(
2458
            sprintf(
2459
                'The "%s" setting can be omitted since is set to its default value',
2460
                $key
2461
            )
2462
        );
2463
    }
2464
}
2465