Passed
Pull Request — master (#285)
by Théo
02:14
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
use function uniqid;
78
79
/**
80
 * @private
81
 */
82
final class Configuration
83
{
84
    private const DEFAULT_ALIAS = 'test.phar';
85
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
86
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
87
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
88
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
89
    private const DEFAULT_BANNER = <<<'BANNER'
90
Generated by Humbug Box.
91
92
@link https://github.com/humbug/box
93
BANNER;
94
    private const FILES_SETTINGS = [
95
        'directories',
96
        'finder',
97
    ];
98
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
99
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
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 $map;
158
    private $fileMapper;
159
    private $metadata;
160
    private $tmpOutputPath;
161
    private $outputPath;
162
    private $privateKeyPassphrase;
163
    private $privateKeyPath;
164
    private $promptForPrivateKey;
165
    private $processedReplacements;
166
    private $shebang;
167
    private $signingAlgorithm;
168
    private $stubBannerContents;
169
    private $stubBannerPath;
170
    private $stubPath;
171
    private $isInterceptFileFuncs;
172
    private $isStubGenerated;
173
    private $checkRequirements;
174
    private $warnings;
175
    private $recommendations;
176
177
    public static function create(?string $file, stdClass $raw): self
178
    {
179
        $logger = new ConfigurationLogger();
180
181
        $alias = self::retrieveAlias($raw);
182
183
        $basePath = self::retrieveBasePath($file, $raw, $logger);
184
185
        $composerFiles = self::retrieveComposerFiles($basePath);
186
187
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
188
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
189
190
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
191
192
        /** @var (string|null)[] $composerJson */
193
        $composerJson = $composerFiles[0];
194
        /** @var (string|null)[] $composerJson */
195
        $composerLock = $composerFiles[1];
196
197
        $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

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