Passed
Pull Request — master (#285)
by Théo
02:18
created

php$0 ➔ addRecommendationForDefaultValue()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
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);
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
        @trigger_error(
638
            'The function "Configuration::isInterceptFileFuncs()" is deprecated, use "Configuration::interceptsFileFuncs()" instead.',
639
            E_USER_DEPRECATED
640
        );
641
642
        return $this->interceptsFileFuncs();
643
    }
644
645
    public function interceptsFileFuncs(): bool
646
    {
647
        return $this->isInterceptFileFuncs;
648
    }
649
650
    public function isStubGenerated(): bool
651
    {
652
        return $this->isStubGenerated;
653
    }
654
655
    /**
656
     * @return string[]
657
     */
658
    public function getWarnings(): array
659
    {
660
        return $this->warnings;
661
    }
662
663
    /**
664
     * @return string[]
665
     */
666
    public function getRecommendations(): array
667
    {
668
        return $this->recommendations;
669
    }
670
671
    private static function retrieveAlias(stdClass $raw): string
672
    {
673
        if (false === isset($raw->{self::ALIAS_KEY})) {
674
            return uniqid('box-auto-generated-alias-', false).'.phar';
675
        }
676
677
        $alias = trim($raw->{self::ALIAS_KEY});
678
679
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
680
681
        return $alias;
682
    }
683
684
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
685
    {
686
        if (null === $file) {
687
            return getcwd();
688
        }
689
690
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
691
            return realpath(dirname($file));
692
        }
693
694
        $basePath = trim($raw->{self::BASE_PATH_KEY});
695
696
        Assertion::directory(
697
            $basePath,
698
            'The base path "%s" is not a directory or does not exist.'
699
        );
700
701
        $basePath = realpath($basePath);
702
        $defaultPath = realpath(dirname($file));
703
704
        if ($basePath === $defaultPath) {
705
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
706
        }
707
708
        return $basePath;
709
    }
710
711
    /**
712
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
713
     */
714
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
715
    {
716
        if (null === $file) {
717
            return true;
718
        }
719
720
        $associativeRaw = (array) $raw;
721
722
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
723
    }
724
725
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
726
    {
727
        if (property_exists($raw, self::AUTO_DISCOVERY_KEY) && false === $raw->{self::AUTO_DISCOVERY_KEY}) {
728
            self::addRecommendationForDefaultValue($logger, self::AUTO_DISCOVERY_KEY);
729
        }
730
731
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
732
    }
733
734
    private static function retrieveBlacklistFilter(
735
        stdClass $raw,
736
        string $basePath,
737
        ConfigurationLogger $logger,
738
        ?string ...$excludedPaths
739
    ): array {
740
        $blacklist = self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths);
741
742
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
743
            if ($file->isLink()) {
744
                return false;
745
            }
746
747
            if (false === $file->getRealPath()) {
748
                return false;
749
            }
750
751
            if (in_array($file->getRealPath(), $blacklist, true)) {
752
                return false;
753
            }
754
755
            return null;
756
        };
757
758
        return [$blacklist, $blacklistFilter];
759
    }
760
761
    /**
762
     * @param stdClass        $raw
763
     * @param string          $basePath
764
     * @param null[]|string[] $excludedPaths
765
     *
766
     * @return string[]
767
     */
768
    private static function retrieveBlacklist(
769
        stdClass $raw,
770
        string $basePath,
771
        ConfigurationLogger $logger,
772
        ?string ...$excludedPaths
773
    ): array {
774
        if (property_exists($raw, self::BLACKLIST_KEY) && [] === $raw->{self::BLACKLIST_KEY}) {
775
            self::addRecommendationForDefaultValue($logger, self::BLACKLIST_KEY);
776
        }
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
        $files = [];
916
917
        if (isset($composerFiles[0][0])) {
918
            $files[] = $composerFiles[0][0];
919
        }
920
921
        if (isset($composerFiles[1][1])) {
922
            $files[] = $composerFiles[1][0];
923
        }
924
925
        if (false === isset($raw->{$key})) {
926
            return $files;
927
        }
928
929
        if ([] === (array) $raw->{$key}) {
930
            self::addRecommendationForDefaultValue($logger, $key);
931
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
        if (isset($raw->{$key})) {
1012
            $finder = $raw->{$key};
1013
1014
            if ([] === $finder) {
1015
                self::addRecommendationForDefaultValue($logger, $key);
1016
1017
                return [];
1018
            }
1019
1020
            return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1021
        }
1022
1023
        return [];
1024
    }
1025
1026
    /**
1027
     * @param iterable[]|SplFileInfo[][] $fileIterators
1028
     *
1029
     * @return SplFileInfo[]
1030
     */
1031
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1032
    {
1033
        $files = [];
1034
1035
        foreach ($fileIterators as $fileIterator) {
1036
            foreach ($fileIterator as $file) {
1037
                $files[(string) $file] = $file;
1038
            }
1039
        }
1040
1041
        return array_values($files);
1042
    }
1043
1044
    /**
1045
     * @param string[] $devPackages
1046
     *
1047
     * @return Finder[]|SplFileInfo[][]
1048
     */
1049
    private static function processFinders(
1050
        array $findersConfig,
1051
        string $basePath,
1052
        Closure $blacklistFilter,
1053
        array $devPackages
1054
    ): array {
1055
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1056
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1057
        };
1058
1059
        return array_map($processFinderConfig, $findersConfig);
1060
    }
1061
1062
    /**
1063
     * @param string[] $devPackages
1064
     *
1065
     * @return Finder|SplFileInfo[]
1066
     */
1067
    private static function processFinder(
1068
        stdClass $config,
1069
        string $basePath,
1070
        Closure $blacklistFilter,
1071
        array $devPackages
1072
    ): Finder {
1073
        $finder = Finder::create()
1074
            ->files()
1075
            ->filter($blacklistFilter)
1076
            ->filter(
1077
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
1078
                    foreach ($devPackages as $devPackage) {
1079
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1080
                            // File belongs to the dev package
1081
                            return false;
1082
                        }
1083
                    }
1084
1085
                    return true;
1086
                }
1087
            )
1088
            ->ignoreVCS(true)
1089
        ;
1090
1091
        $normalizedConfig = (function (array $config, Finder $finder): array {
1092
            $normalizedConfig = [];
1093
1094
            foreach ($config as $method => $arguments) {
1095
                $method = trim($method);
1096
                $arguments = (array) $arguments;
1097
1098
                Assertion::methodExists(
1099
                    $method,
1100
                    $finder,
1101
                    'The method "Finder::%s" does not exist.'
1102
                );
1103
1104
                $normalizedConfig[$method] = $arguments;
1105
            }
1106
1107
            krsort($normalizedConfig);
1108
1109
            return $normalizedConfig;
1110
        })((array) $config, $finder);
1111
1112
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
1113
            $directory = self::normalizePath($directory, $basePath);
1114
1115
            Assertion::false(
1116
                is_link($directory),
1117
                sprintf(
1118
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1119
                    $directory
1120
                )
1121
            );
1122
1123
            Assertion::directory($directory);
1124
1125
            return $directory;
1126
        };
1127
1128
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1129
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
1130
1131
            Assertion::false(
1132
                is_link($fileOrDirectory),
1133
                sprintf(
1134
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1135
                    $fileOrDirectory
1136
                )
1137
            );
1138
1139
            Assertion::false(
1140
                file_exists($fileOrDirectory),
1141
                sprintf(
1142
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1143
                    $fileOrDirectory
1144
                )
1145
            );
1146
1147
            if (false === is_file($fileOrDirectory)) {
1148
                Assertion::directory($fileOrDirectory);
1149
            } else {
1150
                Assertion::file($fileOrDirectory);
1151
            }
1152
1153
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1154
                $fileOrDirectory = null;
1155
            }
1156
        };
1157
1158
        foreach ($normalizedConfig as $method => $arguments) {
1159
            if ('in' === $method) {
1160
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1161
            }
1162
1163
            if ('exclude' === $method) {
1164
                $arguments = array_unique(array_map('trim', $arguments));
1165
            }
1166
1167
            if ('append' === $method) {
1168
                array_walk($arguments, $normalizeFileOrDirectory);
1169
1170
                $arguments = [array_filter($arguments)];
1171
            }
1172
1173
            foreach ($arguments as $argument) {
1174
                $finder->$method($argument);
1175
            }
1176
        }
1177
1178
        return $finder;
1179
    }
1180
1181
    /**
1182
     * @param string[] $devPackages
1183
     * @param string[] $filesToAppend
1184
     *
1185
     * @return string[][]
1186
     */
1187
    private static function retrieveAllDirectoriesToInclude(
1188
        string $basePath,
1189
        ?array $decodedJsonContents,
1190
        array $devPackages,
1191
        array $filesToAppend,
1192
        array $excludedPaths
1193
    ): array {
1194
        $toString = function ($file): string {
1195
            // @param string|SplFileInfo $file
1196
            return (string) $file;
1197
        };
1198
1199
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1200
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1201
        } else {
1202
            $vendorDir = self::normalizePath('vendor', $basePath);
1203
        }
1204
1205
        if (file_exists($vendorDir)) {
1206
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1207
            // dependencies are included in the `composer.json`
1208
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1209
1210
            if (file_exists($installedJsonFiles)) {
1211
                $filesToAppend[] = $installedJsonFiles;
1212
            }
1213
1214
            $vendorPackages = toArray(values(map(
1215
                $toString,
1216
                Finder::create()
1217
                    ->in($vendorDir)
1218
                    ->directories()
1219
                    ->depth(1)
1220
                    ->ignoreUnreadableDirs()
1221
                    ->filter(
1222
                        function (SplFileInfo $fileInfo): ?bool {
1223
                            if ($fileInfo->isLink()) {
1224
                                return false;
1225
                            }
1226
1227
                            return null;
1228
                        }
1229
                    )
1230
            )));
1231
1232
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1233
1234
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1235
                $files = toArray(values(map(
1236
                    $toString,
1237
                    Finder::create()
1238
                        ->in($basePath)
1239
                        ->files()
1240
                        ->depth(0)
1241
                )));
1242
1243
                $directories = toArray(values(map(
1244
                    $toString,
1245
                    Finder::create()
1246
                        ->in($basePath)
1247
                        ->notPath('vendor')
1248
                        ->directories()
1249
                        ->depth(0)
1250
                )));
1251
1252
                return [
1253
                    array_merge($files, $filesToAppend),
1254
                    array_merge($directories, $vendorPackages),
1255
                ];
1256
            }
1257
1258
            $paths = $vendorPackages;
1259
        } else {
1260
            $paths = [];
1261
        }
1262
1263
        $autoload = $decodedJsonContents['autoload'] ?? [];
1264
1265
        if (array_key_exists('psr-4', $autoload)) {
1266
            foreach ($autoload['psr-4'] as $path) {
1267
                /** @var string|string[] $path */
1268
                $composerPaths = (array) $path;
1269
1270
                foreach ($composerPaths as $composerPath) {
1271
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1272
                }
1273
            }
1274
        }
1275
1276
        if (array_key_exists('psr-0', $autoload)) {
1277
            foreach ($autoload['psr-0'] as $path) {
1278
                /** @var string|string[] $path */
1279
                $composerPaths = (array) $path;
1280
1281
                foreach ($composerPaths as $composerPath) {
1282
                    if ('' !== trim($composerPath)) {
1283
                        $paths[] = $composerPath;
1284
                    }
1285
                }
1286
            }
1287
        }
1288
1289
        if (array_key_exists('classmap', $autoload)) {
1290
            foreach ($autoload['classmap'] as $path) {
1291
                // @var string $path
1292
                $paths[] = $path;
1293
            }
1294
        }
1295
1296
        $normalizePath = function (string $path) use ($basePath): string {
1297
            return is_absolute_path($path)
1298
                ? canonicalize($path)
1299
                : self::normalizePath(trim($path, '/ '), $basePath)
1300
            ;
1301
        };
1302
1303
        if (array_key_exists('files', $autoload)) {
1304
            foreach ($autoload['files'] as $path) {
1305
                // @var string $path
1306
                $path = $normalizePath($path);
1307
1308
                Assertion::file($path);
1309
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1310
1311
                $filesToAppend[] = $path;
1312
            }
1313
        }
1314
1315
        $files = $filesToAppend;
1316
        $directories = [];
1317
1318
        foreach ($paths as $path) {
1319
            $path = $normalizePath($path);
1320
1321
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1322
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1323
1324
            if (is_file($path)) {
1325
                $files[] = $path;
1326
            } else {
1327
                $directories[] = $path;
1328
            }
1329
        }
1330
1331
        [$files, $directories] = [
1332
            array_unique($files),
1333
            array_unique($directories),
1334
        ];
1335
1336
        return [
1337
            array_diff($files, $excludedPaths),
1338
            array_diff($directories, $excludedPaths),
1339
        ];
1340
    }
1341
1342
    /**
1343
     * @param string[] $files
1344
     * @param string[] $directories
1345
     * @param string[] $excludedPaths
1346
     * @param string[] $devPackages
1347
     *
1348
     * @return SplFileInfo[]
1349
     */
1350
    private static function retrieveAllFiles(
1351
        string $basePath,
1352
        array $directories,
1353
        ?string $mainScriptPath,
1354
        Closure $blacklistFilter,
1355
        array $excludedPaths,
1356
        array $devPackages
1357
    ): iterable {
1358
        if ([] === $directories) {
1359
            return [];
1360
        }
1361
1362
        $relativeDevPackages = array_map(
1363
            function (string $packagePath) use ($basePath): string {
1364
                return make_path_relative($packagePath, $basePath);
1365
            },
1366
            $devPackages
1367
        );
1368
1369
        $finder = Finder::create()
1370
            ->files()
1371
            ->filter($blacklistFilter)
1372
            ->exclude($relativeDevPackages)
1373
            ->ignoreVCS(true)
1374
            ->ignoreDotFiles(true)
1375
            // Remove build files
1376
            ->notName('composer.json')
1377
            ->notName('composer.lock')
1378
            ->notName('Makefile')
1379
            ->notName('Vagrantfile')
1380
            ->notName('phpstan*.neon*')
1381
            ->notName('infection*.json*')
1382
            ->notName('humbug*.json*')
1383
            ->notName('easy-coding-standard.neon*')
1384
            ->notName('phpbench.json*')
1385
            ->notName('phpcs.xml*')
1386
            ->notName('psalm.xml*')
1387
            ->notName('scoper.inc*')
1388
            ->notName('box*.json*')
1389
            ->notName('phpdoc*.xml*')
1390
            ->notName('codecov.yml*')
1391
            ->notName('Dockerfile')
1392
            ->exclude('build')
1393
            ->exclude('dist')
1394
            ->exclude('example')
1395
            ->exclude('examples')
1396
            // Remove documentation
1397
            ->notName('*.md')
1398
            ->notName('*.rst')
1399
            ->notName('/^readme(\..*+)?$/i')
1400
            ->notName('/^upgrade(\..*+)?$/i')
1401
            ->notName('/^contributing(\..*+)?$/i')
1402
            ->notName('/^changelog(\..*+)?$/i')
1403
            ->notName('/^authors?(\..*+)?$/i')
1404
            ->notName('/^conduct(\..*+)?$/i')
1405
            ->notName('/^todo(\..*+)?$/i')
1406
            ->exclude('doc')
1407
            ->exclude('docs')
1408
            ->exclude('documentation')
1409
            // Remove backup files
1410
            ->notName('*~')
1411
            ->notName('*.back')
1412
            ->notName('*.swp')
1413
            // Remove tests
1414
            ->notName('*Test.php')
1415
            ->exclude('test')
1416
            ->exclude('Test')
1417
            ->exclude('tests')
1418
            ->exclude('Tests')
1419
            ->notName('/phpunit.*\.xml(.dist)?/')
1420
            ->notName('/behat.*\.yml(.dist)?/')
1421
            ->exclude('spec')
1422
            ->exclude('specs')
1423
            ->exclude('features')
1424
            // Remove CI config
1425
            ->exclude('travis')
1426
            ->notName('travis.yml')
1427
            ->notName('appveyor.yml')
1428
            ->notName('build.xml*')
1429
        ;
1430
1431
        if (null !== $mainScriptPath) {
1432
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1433
        }
1434
1435
        $finder->in($directories);
1436
1437
        $excludedPaths = array_unique(
1438
            array_filter(
1439
                array_map(
1440
                    function (string $path) use ($basePath): string {
1441
                        return make_path_relative($path, $basePath);
1442
                    },
1443
                    $excludedPaths
1444
                ),
1445
                function (string $path): bool {
1446
                    return '..' !== substr($path, 0, 2);
1447
                }
1448
            )
1449
        );
1450
1451
        foreach ($excludedPaths as $excludedPath) {
1452
            $finder->notPath($excludedPath);
1453
        }
1454
1455
        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...
1456
    }
1457
1458
    /**
1459
     * @param stdClass $raw
1460
     * @param string   $key      Config property name
1461
     * @param string   $basePath
1462
     *
1463
     * @return string[]
1464
     */
1465
    private static function retrieveDirectoryPaths(
1466
        stdClass $raw,
1467
        string $key,
1468
        string $basePath,
1469
        ConfigurationLogger $logger
1470
    ): array {
1471
        if (false === isset($raw->{$key})) {
1472
            return [];
1473
        }
1474
1475
        $directories = $raw->{$key};
1476
1477
        if ([] === $directories) {
1478
            self::addRecommendationForDefaultValue($logger, $key);
1479
1480
            return [];
1481
        }
1482
1483
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1484
            $directory = self::normalizePath($directory, $basePath);
1485
1486
            Assertion::false(
1487
                is_link($directory),
1488
                sprintf(
1489
                    'Cannot add the link "%s": links are not supported.',
1490
                    $directory
1491
                )
1492
            );
1493
1494
            Assertion::directory(
1495
                $directory,
1496
                sprintf(
1497
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1498
                    $key
1499
                )
1500
            );
1501
1502
            return $directory;
1503
        };
1504
1505
        return array_map($normalizeDirectory, $directories);
1506
    }
1507
1508
    private static function normalizePath(string $file, string $basePath): string
1509
    {
1510
        return make_path_absolute(trim($file), $basePath);
1511
    }
1512
1513
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1514
    {
1515
        if (property_exists($raw, self::DUMP_AUTOLOAD_KEY) && true === $raw->{self::DUMP_AUTOLOAD_KEY}) {
1516
            self::addRecommendationForDefaultValue($logger, self::DUMP_AUTOLOAD_KEY);
1517
        }
1518
1519
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1520
            return $composerJson;
1521
        }
1522
1523
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1524
1525
        if (false === $composerJson && $dumpAutoload) {
1526
            $logger->addWarning(
1527
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1528
                .' for it could not be found'
1529
            );
1530
1531
            return false;
1532
        }
1533
1534
        return $composerJson && false !== $dumpAutoload;
1535
    }
1536
1537
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1538
    {
1539
        if (property_exists($raw, self::EXCLUDE_COMPOSER_FILES_KEY)
1540
            && (null === $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} || true === $raw->{self::EXCLUDE_COMPOSER_FILES_KEY})
1541
        ) {
1542
            self::addRecommendationForDefaultValue($logger, self::EXCLUDE_COMPOSER_FILES_KEY);
1543
        }
1544
1545
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1546
    }
1547
1548
    /**
1549
     * @return Compactor[]
1550
     */
1551
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1552
    {
1553
        if (property_exists($raw, self::COMPACTORS_KEY)
1554
            && (null === $raw->{self::COMPACTORS_KEY} || [] === $raw->{self::COMPACTORS_KEY})
1555
        ) {
1556
            self::addRecommendationForDefaultValue($logger, self::COMPACTORS_KEY);
1557
        }
1558
1559
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1560
            return [];
1561
        }
1562
1563
        $compactorClasses = array_unique((array) $raw->{self::COMPACTORS_KEY});
1564
1565
        return array_map(
1566
            function (string $class) use ($raw, $basePath, $logger): Compactor {
1567
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1568
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1569
1570
                if (Php::class === $class || LegacyPhp::class === $class) {
1571
                    return self::createPhpCompactor($raw);
1572
                }
1573
1574
                if (PhpScoperCompactor::class === $class) {
1575
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
1576
1577
                    $prefix = null === $phpScoperConfig->getPrefix()
1578
                        ? uniqid('_HumbugBox', false)
1579
                        : $phpScoperConfig->getPrefix()
1580
                    ;
1581
1582
                    return new PhpScoperCompactor(
1583
                        new SimpleScoper(
1584
                            (new class() extends ApplicationFactory {
1585
                                public static function createScoper(): Scoper
1586
                                {
1587
                                    return parent::createScoper();
1588
                                }
1589
                            })::createScoper(),
1590
                            $prefix,
1591
                            $phpScoperConfig->getWhitelist(),
1592
                            $phpScoperConfig->getPatchers()
1593
                        )
1594
                    );
1595
                }
1596
1597
                return new $class();
1598
            },
1599
            $compactorClasses
1600
        );
1601
    }
1602
1603
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1604
    {
1605
        if (property_exists($raw, self::COMPRESSION_KEY) && null === $raw->{self::COMPRESSION_KEY}) {
1606
            self::addRecommendationForDefaultValue($logger, self::COMPRESSION_KEY);
1607
        }
1608
1609
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1610
            return null;
1611
        }
1612
1613
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1614
1615
        Assertion::inArray(
1616
            $raw->{self::COMPRESSION_KEY},
1617
            $knownAlgorithmNames,
1618
            sprintf(
1619
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1620
                implode('", "', $knownAlgorithmNames)
1621
            )
1622
        );
1623
1624
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1625
1626
        // Phar::NONE is not valid for compressFiles()
1627
        if (Phar::NONE === $value) {
1628
            return null;
1629
        }
1630
1631
        return $value;
1632
    }
1633
1634
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1635
    {
1636
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1637
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1638
        }
1639
1640
        $defaultChmod = intval(0755, 8);
1641
1642
        if (isset($raw->{self::CHMOD_KEY})) {
1643
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1644
1645
            if ($defaultChmod === $chmod) {
1646
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1647
            }
1648
1649
            return $chmod;
1650
        }
1651
1652
        return $defaultChmod;
1653
    }
1654
1655
    private static function retrieveMainScriptPath(
1656
        stdClass $raw,
1657
        string $basePath,
1658
        ?array $decodedJsonContents,
1659
        ConfigurationLogger $logger
1660
    ): ?string {
1661
        $firstBin = false;
1662
1663
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1664
            /** @var false|string $firstBin */
1665
            $firstBin = current((array) $decodedJsonContents['bin']);
1666
1667
            if (false !== $firstBin) {
1668
                $firstBin = self::normalizePath($firstBin, $basePath);
1669
            }
1670
        }
1671
1672
        if (isset($raw->{self::MAIN_KEY})) {
1673
            $main = $raw->{self::MAIN_KEY};
1674
1675
            if (is_string($main)) {
1676
                $main = self::normalizePath($main, $basePath);
1677
1678
                if ($main === $firstBin) {
1679
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1680
                }
1681
            }
1682
        } else {
1683
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1684
        }
1685
1686
        if (is_bool($main)) {
1687
            Assertion::false(
1688
                $main,
1689
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1690
            );
1691
1692
            return null;
1693
        }
1694
1695
        Assertion::file($main);
1696
1697
        return $main;
1698
    }
1699
1700
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1701
    {
1702
        if (null === $mainScriptPath) {
1703
            return null;
1704
        }
1705
1706
        $contents = file_contents($mainScriptPath);
1707
1708
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1709
        // PHAR entry point file.
1710
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1711
        // case the main script can be ignored or disabled.
1712
        return preg_replace('/^#!.*\s*/', '', $contents);
1713
    }
1714
1715
    /**
1716
     * @return string|null[][]
1717
     */
1718
    private static function retrieveComposerFiles(string $basePath): array
1719
    {
1720
        $retrieveFileAndContents = function (string $file): array {
1721
            $json = new Json();
1722
1723
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1724
                return [null, null];
1725
            }
1726
1727
            try {
1728
                $contents = $json->decodeFile($file, true);
1729
            } catch (ParsingException $exception) {
1730
                throw new InvalidArgumentException(
1731
                    sprintf(
1732
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1733
                        $file,
1734
                        $exception->getMessage()
1735
                    ),
1736
                    0,
1737
                    $exception
1738
                );
1739
            }
1740
1741
            return [$file, $contents];
1742
        };
1743
1744
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1745
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1746
1747
        return [
1748
            [$composerJson, $composerJsonContents],
1749
            [$composerLock, $composerLockContents],
1750
        ];
1751
    }
1752
1753
    /**
1754
     * @return string[][]
1755
     */
1756
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1757
    {
1758
        if (false === isset($raw->{self::MAP_KEY})) {
1759
            return [];
1760
        }
1761
1762
        $map = [];
1763
        $rawMap = (array) $raw->{self::MAP_KEY};
1764
1765
        if ([] === $rawMap) {
1766
            self::addRecommendationForDefaultValue($logger, self::MAP_KEY);
1767
        }
1768
1769
        foreach ($rawMap as $item) {
1770
            $processed = [];
1771
1772
            foreach ($item as $match => $replace) {
1773
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1774
            }
1775
1776
            if (isset($processed['_empty_'])) {
1777
                $processed[''] = $processed['_empty_'];
1778
1779
                unset($processed['_empty_']);
1780
            }
1781
1782
            $map[] = $processed;
1783
        }
1784
1785
        return $map;
1786
    }
1787
1788
    /**
1789
     * @return mixed
1790
     */
1791
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1792
    {
1793
        if (property_exists($raw, self::METADATA_KEY) && null === $raw->{self::METADATA_KEY}) {
1794
            self::addRecommendationForDefaultValue($logger, self::METADATA_KEY);
1795
        }
1796
1797
        if (false === isset($raw->{self::METADATA_KEY})) {
1798
            return null;
1799
        }
1800
1801
        $metadata = $raw->{self::METADATA_KEY};
1802
1803
        return is_object($metadata) ? (array) $metadata : $metadata;
1804
    }
1805
1806
    /**
1807
     * @return string[] The first element is the temporary output path and the second the final one
1808
     */
1809
    private static function retrieveOutputPath(
1810
        stdClass $raw,
1811
        string $basePath,
1812
        ?string $mainScriptPath,
1813
        ConfigurationLogger $logger
1814
    ): array {
1815
        $defaultPath = null;
1816
1817
        if (null !== $mainScriptPath
1818
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)
1819
        ) {
1820
            $defaultPath = $matches['main'].'.phar';
1821
        }
1822
1823
        if (isset($raw->{self::OUTPUT_KEY})) {
1824
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1825
1826
            if ($path === $defaultPath) {
1827
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1828
            }
1829
        } elseif (null !== $defaultPath) {
1830
            $path = $defaultPath;
1831
        } else {
1832
            // Last resort, should not happen
1833
            $path = self::normalizePath(self::DEFAULT_ALIAS, $basePath);
1834
        }
1835
1836
        $tmp = $real = $path;
1837
1838
        if ('.phar' !== substr($real, -5)) {
1839
            $tmp .= '.phar';
1840
        }
1841
1842
        return [$tmp, $real];
1843
    }
1844
1845
    private static function retrievePrivateKeyPath(
1846
        stdClass $raw,
1847
        string $basePath,
1848
        int $signingAlgorithm,
1849
        ConfigurationLogger $logger
1850
    ): ?string {
1851
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1852
            if (null === $raw->{self::KEY_KEY}) {
1853
                $logger->addRecommendation(
1854
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1855
                );
1856
            } else {
1857
                $logger->addWarning(
1858
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1859
                );
1860
            }
1861
1862
            return null;
1863
        }
1864
1865
        if (!isset($raw->{self::KEY_KEY})) {
1866
            Assertion::true(
1867
                Phar::OPENSSL !== $signingAlgorithm,
1868
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1869
            );
1870
1871
            return null;
1872
        }
1873
1874
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1875
1876
        Assertion::file($path);
1877
1878
        return $path;
1879
    }
1880
1881
    private static function retrievePrivateKeyPassphrase(
1882
        stdClass $raw,
1883
        int $algorithm,
1884
        ConfigurationLogger $logger
1885
    ): ?string {
1886
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1887
            return null;
1888
        }
1889
1890
        /** @var null|false|string $keyPass */
1891
        $keyPass = $raw->{self::KEY_PASS_KEY};
1892
1893
        if (Phar::OPENSSL !== $algorithm) {
1894
            if (false === $keyPass || null === $keyPass) {
1895
                $logger->addRecommendation(
1896
                    sprintf(
1897
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1898
                        .'not "OPENSSL".',
1899
                        self::KEY_PASS_KEY
1900
                    )
1901
                );
1902
            } else {
1903
                $logger->addWarning(
1904
                    sprintf(
1905
                    'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1906
                        self::KEY_PASS_KEY
1907
                    )
1908
                );
1909
            }
1910
1911
            return null;
1912
        }
1913
1914
        return is_string($keyPass) ? $keyPass : null;
1915
    }
1916
1917
    /**
1918
     * @return scalar[]
1919
     */
1920
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1921
    {
1922
        if (property_exists($raw, self::REPLACEMENTS_KEY) && new stdClass() == $raw->{self::REPLACEMENTS_KEY}) {
1923
            self::addRecommendationForDefaultValue($logger, self::REPLACEMENTS_KEY);
1924
        }
1925
1926
        if (null === $file) {
1927
            return [];
1928
        }
1929
1930
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1931
1932
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1933
            $replacements[$git] = self::retrievePrettyGitTag($file);
1934
        }
1935
1936
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1937
            $replacements[$git] = self::retrieveGitHash($file);
1938
        }
1939
1940
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
1941
            $replacements[$git] = self::retrieveGitHash($file, true);
1942
        }
1943
1944
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
1945
            $replacements[$git] = self::retrieveGitTag($file);
1946
        }
1947
1948
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
1949
            $replacements[$git] = self::retrieveGitVersion($file);
1950
        }
1951
1952
        /**
1953
         * @var string
1954
         * @var bool   $valueSetByUser
1955
         */
1956
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
1957
1958
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
1959
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
1960
        } elseif ($valueSetByUser) {
1961
            $logger->addRecommendation(
1962
                sprintf(
1963
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
1964
                    self::DATETIME_FORMAT_KEY,
1965
                    self::DATETIME_KEY
1966
                )
1967
            );
1968
        }
1969
1970
        $sigil = self::retrieveReplacementSigil($raw, $logger);
1971
1972
        foreach ($replacements as $key => $value) {
1973
            unset($replacements[$key]);
1974
            $replacements[$sigil.$key.$sigil] = $value;
1975
        }
1976
1977
        return $replacements;
1978
    }
1979
1980
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1981
    {
1982
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
1983
    }
1984
1985
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1986
    {
1987
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
1988
    }
1989
1990
    /**
1991
     * @param string $file
1992
     * @param bool   $short Use the short version
1993
     *
1994
     * @return string the commit hash
1995
     */
1996
    private static function retrieveGitHash(string $file, bool $short = false): string
1997
    {
1998
        return self::runGitCommand(
1999
            sprintf(
2000
                'git log --pretty="%s" -n1 HEAD',
2001
                $short ? '%h' : '%H'
2002
            ),
2003
            $file
2004
        );
2005
    }
2006
2007
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2008
    {
2009
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2010
    }
2011
2012
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2013
    {
2014
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2015
    }
2016
2017
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2018
    {
2019
        if (property_exists($raw, $key) && null === $raw->{$key}) {
2020
            self::addRecommendationForDefaultValue($logger, $key);
2021
        }
2022
2023
        return $raw->{$key} ?? null;
2024
    }
2025
2026
    private static function retrieveGitTag(string $file): string
2027
    {
2028
        return self::runGitCommand('git describe --tags HEAD', $file);
2029
    }
2030
2031
    private static function retrievePrettyGitTag(string $file): string
2032
    {
2033
        $version = self::retrieveGitTag($file);
2034
2035
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2036
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2037
        }
2038
2039
        return $version;
2040
    }
2041
2042
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2043
    {
2044
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2045
    }
2046
2047
    private static function retrieveGitVersion(string $file): ?string
2048
    {
2049
        try {
2050
            return self::retrieveGitTag($file);
2051
        } catch (RuntimeException $exception) {
2052
            try {
2053
                return self::retrieveGitHash($file, true);
2054
            } catch (RuntimeException $exception) {
2055
                throw new RuntimeException(
2056
                    sprintf(
2057
                        'The tag or commit hash could not be retrieved from "%s": %s',
2058
                        dirname($file),
2059
                        $exception->getMessage()
2060
                    ),
2061
                    0,
2062
                    $exception
2063
                );
2064
            }
2065
        }
2066
    }
2067
2068
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2069
    {
2070
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2071
    }
2072
2073
    private static function retrieveDatetimeNow(string $format): string
2074
    {
2075
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2076
2077
        return $now->format($format);
2078
    }
2079
2080
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2081
    {
2082
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2083
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2084
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2085
            @trigger_error(
2086
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2087
                E_USER_DEPRECATED
2088
            );
2089
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2090
2091
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2092
        } else {
2093
            $format = null;
2094
        }
2095
        /** @var null|string $format */
2096
        if (self::DEFAULT_DATETIME_FORMAT === $format) {
2097
            self::addRecommendationForDefaultValue($logger, self::DATETIME_FORMAT_KEY);
2098
        }
2099
2100
        if (null !== $format) {
2101
            $formattedDate = (new DateTimeImmutable())->format($format);
2102
2103
            Assertion::false(
2104
                false === $formattedDate || $formattedDate === $format,
2105
                sprintf(
2106
                    'Expected the datetime format to be a valid format: "%s" is not',
2107
                    $format
2108
                )
2109
            );
2110
2111
            return [$format, true];
2112
        }
2113
2114
        return [self::DEFAULT_DATETIME_FORMAT, false];
2115
    }
2116
2117
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2118
    {
2119
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2120
    }
2121
2122
    private static function retrieveShebang(stdClass $raw, ConfigurationLogger $logger): ?string
2123
    {
2124
        if (false === array_key_exists(self::SHEBANG_KEY, (array) $raw)) {
2125
            return self::DEFAULT_SHEBANG;
2126
        }
2127
2128
        $shebang = $raw->{self::SHEBANG_KEY};
2129
2130
        if (false === $shebang) {
2131
            return null;
2132
        }
2133
2134
        if (null === $shebang) {
2135
            self::addRecommendationForDefaultValue($logger, self::SHEBANG_KEY);
2136
2137
            $shebang = self::DEFAULT_SHEBANG;
2138
        }
2139
2140
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2141
2142
        $shebang = trim($shebang);
2143
2144
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2145
        Assertion::true(
2146
            '#!' === substr($shebang, 0, 2),
2147
            sprintf(
2148
                'The shebang line must start with "#!". Got "%s" instead',
2149
                $shebang
2150
            )
2151
        );
2152
2153
        if (self::DEFAULT_SHEBANG === $shebang) {
2154
            self::addRecommendationForDefaultValue($logger, self::SHEBANG_KEY);
2155
        }
2156
2157
        return $shebang;
2158
    }
2159
2160
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2161
    {
2162
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2163
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2164
        }
2165
2166
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2167
            return self::DEFAULT_SIGNING_ALGORITHM;
2168
        }
2169
2170
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2171
2172
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2173
2174
        Assertion::true(
2175
            defined('Phar::'.$algorithm),
2176
            sprintf(
2177
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2178
                $algorithm
2179
            )
2180
        );
2181
2182
        $algorithm = constant('Phar::'.$algorithm);
2183
2184
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2185
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2186
        }
2187
2188
        return $algorithm;
2189
    }
2190
2191
    private static function retrieveStubBannerContents(stdClass $raw, ConfigurationLogger $logger): ?string
2192
    {
2193
        if (property_exists($raw, self::BANNER_KEY) && null === $raw->{self::BANNER_KEY}) {
2194
            self::addRecommendationForDefaultValue($logger, self::BANNER_KEY);
2195
        }
2196
2197
        if (false === isset($raw->{self::BANNER_KEY})) {
2198
            return self::DEFAULT_BANNER;
2199
        }
2200
2201
        $banner = $raw->{self::BANNER_KEY};
2202
2203
        if (self::DEFAULT_BANNER === $banner) {
2204
            self::addRecommendationForDefaultValue($logger, self::BANNER_KEY);
2205
        }
2206
2207
        if (false === $banner) {
2208
            return null;
2209
        }
2210
2211
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2212
2213
        if (is_array($banner)) {
2214
            $banner = implode("\n", $banner);
2215
        }
2216
2217
        return $banner;
2218
    }
2219
2220
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2221
    {
2222
        if (property_exists($raw, self::BANNER_FILE_KEY) && null === $raw->{self::BANNER_FILE_KEY}) {
2223
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
2224
        }
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
        return $bannerFile;
2235
    }
2236
2237
    private static function normalizeStubBannerContents(?string $contents): ?string
2238
    {
2239
        if (null === $contents) {
2240
            return null;
2241
        }
2242
2243
        $banner = explode("\n", $contents);
2244
        $banner = array_map('trim', $banner);
2245
2246
        return implode("\n", $banner);
2247
    }
2248
2249
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2250
    {
2251
        if (property_exists($raw, self::STUB_KEY) && null === $raw->{self::STUB_KEY}) {
2252
            self::addRecommendationForDefaultValue($logger, self::STUB_KEY);
2253
        }
2254
2255
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2256
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2257
2258
            Assertion::file($stubPath);
2259
2260
            return $stubPath;
2261
        }
2262
2263
        return null;
2264
    }
2265
2266
    private static function retrieveInterceptsFileFuncs(stdClass $raw, ConfigurationLogger $logger): bool
2267
    {
2268
        if (property_exists($raw, self::INTERCEPT_KEY)
2269
            && (null === $raw->{self::INTERCEPT_KEY} || false === $raw->{self::INTERCEPT_KEY})
2270
        ) {
2271
            self::addRecommendationForDefaultValue($logger, self::INTERCEPT_KEY);
2272
        }
2273
2274
        return $raw->{self::INTERCEPT_KEY} ?? false;
2275
    }
2276
2277
    private static function retrievePromptForPrivateKey(
2278
        stdClass $raw,
2279
        int $signingAlgorithm,
2280
        ConfigurationLogger $logger
2281
    ): bool {
2282
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2283
            if (Phar::OPENSSL !== $signingAlgorithm) {
2284
                $logger->addWarning(
2285
                    'A prompt for password for the private key has been requested but ignored since the signing '
2286
                    .'algorithm used is not "OPENSSL.'
2287
                );
2288
2289
                return false;
2290
            }
2291
2292
            return true;
2293
        }
2294
2295
        return false;
2296
    }
2297
2298
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
2299
    {
2300
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2301
    }
2302
2303
    private static function retrieveCheckRequirements(
2304
        stdClass $raw,
2305
        bool $hasComposerJson,
2306
        bool $hasComposerLock,
2307
        ConfigurationLogger $logger
2308
    ): bool {
2309
        if (property_exists($raw, self::CHECK_REQUIREMENTS_KEY)
2310
            && (null === $raw->{self::CHECK_REQUIREMENTS_KEY} || true === $raw->{self::CHECK_REQUIREMENTS_KEY})
2311
        ) {
2312
            self::addRecommendationForDefaultValue($logger, self::CHECK_REQUIREMENTS_KEY);
2313
        }
2314
2315
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2316
            return $hasComposerJson || $hasComposerLock;
2317
        }
2318
2319
        /** @var null|bool $checkRequirements */
2320
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2321
2322
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2323
            $logger->addWarning(
2324
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2325
                .'be found.'
2326
            );
2327
2328
            return false;
2329
        }
2330
2331
        return $checkRequirements;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $checkRequirements could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
2332
    }
2333
2334
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2335
    {
2336
        // TODO: add recommendations regarding the order
2337
        if (property_exists($raw, self::PHP_SCOPER_KEY)
2338
            && (null === $raw->{self::PHP_SCOPER_KEY} || self::PHP_SCOPER_CONFIG === $raw->{self::PHP_SCOPER_KEY})
2339
        ) {
2340
            self::addRecommendationForDefaultValue($logger, self::PHP_SCOPER_KEY);
2341
        }
2342
2343
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2344
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2345
2346
            return file_exists($configFilePath)
2347
                ? PhpScoperConfiguration::load($configFilePath)
2348
                : PhpScoperConfiguration::load()
2349
             ;
2350
        }
2351
2352
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2353
2354
        Assertion::string($configFile);
2355
2356
        $configFilePath = make_path_absolute($configFile, $basePath);
2357
2358
        Assertion::file($configFilePath);
2359
        Assertion::readable($configFilePath);
2360
2361
        return PhpScoperConfiguration::load($configFilePath);
2362
    }
2363
2364
    /**
2365
     * Runs a Git command on the repository.
2366
     *
2367
     * @param string $command the command
2368
     *
2369
     * @return string the trimmed output from the command
2370
     */
2371
    private static function runGitCommand(string $command, string $file): string
2372
    {
2373
        $path = dirname($file);
2374
2375
        $process = new Process($command, $path);
2376
2377
        if (0 === $process->run()) {
2378
            return trim($process->getOutput());
2379
        }
2380
2381
        throw new RuntimeException(
2382
            sprintf(
2383
                'The tag or commit hash could not be retrieved from "%s": %s',
2384
                $path,
2385
                $process->getErrorOutput()
2386
            )
2387
        );
2388
    }
2389
2390
    private static function createPhpCompactor(stdClass $raw): Compactor
2391
    {
2392
        // TODO: false === not set; check & add test/doc
2393
        $tokenizer = new Tokenizer();
2394
2395
        if (false === empty($raw->{self::ANNOTATIONS_KEY}) && isset($raw->{self::ANNOTATIONS_KEY}->ignore)) {
2396
            $tokenizer->ignore(
2397
                (array) $raw->{self::ANNOTATIONS_KEY}->ignore
2398
            );
2399
        }
2400
2401
        return new Php($tokenizer);
2402
    }
2403
2404
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2405
    {
2406
        $logger->addRecommendation(
2407
            sprintf(
2408
                'The "%s" setting can be omitted since is set to its default value',
2409
                $key
2410
            )
2411
        );
2412
    }
2413
}
2414