Passed
Pull Request — master (#285)
by Théo
05:40
created

Configuration::retrieveFilesFromFinders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 21
rs 10
c 0
b 0
f 0
nc 3
nop 6
cc 3
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 phpDocumentor\Reflection\Types\Self_;
34
use function property_exists;
35
use function realpath;
36
use RuntimeException;
37
use Seld\JsonLint\ParsingException;
38
use SplFileInfo;
39
use stdClass;
40
use Symfony\Component\Finder\Finder;
41
use Symfony\Component\Process\Process;
42
use const E_USER_DEPRECATED;
43
use function array_column;
44
use function array_diff;
45
use function array_filter;
46
use function array_key_exists;
47
use function array_keys;
48
use function array_map;
49
use function array_merge;
50
use function array_unique;
51
use function constant;
52
use function defined;
53
use function dirname;
54
use function file_exists;
55
use function in_array;
56
use function intval;
57
use function is_array;
58
use function is_bool;
59
use function is_file;
60
use function is_link;
61
use function is_object;
62
use function is_readable;
63
use function is_string;
64
use function iter\map;
65
use function iter\toArray;
66
use function iter\values;
67
use function KevinGH\Box\FileSystem\canonicalize;
68
use function KevinGH\Box\FileSystem\file_contents;
69
use function KevinGH\Box\FileSystem\is_absolute_path;
70
use function KevinGH\Box\FileSystem\longest_common_base_path;
71
use function KevinGH\Box\FileSystem\make_path_absolute;
72
use function KevinGH\Box\FileSystem\make_path_relative;
73
use function preg_match;
74
use function sprintf;
75
use function strtoupper;
76
use function substr;
77
use function trigger_error;
78
use function uniqid;
79
80
/**
81
 * @private
82
 */
83
final class Configuration
84
{
85
    private const DEFAULT_ALIAS = 'test.phar';
86
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
87
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
88
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
89
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
90
    private const DEFAULT_BANNER = <<<'BANNER'
91
Generated by Humbug Box.
92
93
@link https://github.com/humbug/box
94
BANNER;
95
    private const FILES_SETTINGS = [
96
        'directories',
97
        'finder',
98
    ];
99
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
100
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
101
102
    private const ALGORITHM_KEY = 'algorithm';
103
    private const ALIAS_KEY = 'alias';
104
    private const ANNOTATIONS_KEY = 'annotations';
105
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
106
    private const BANNER_KEY = 'banner';
107
    private const BANNER_FILE_KEY = 'banner-file';
108
    private const BASE_PATH_KEY = 'base-path';
109
    private const BLACKLIST_KEY = 'blacklist';
110
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
111
    private const CHMOD_KEY = 'chmod';
112
    private const COMPACTORS_KEY = 'compactors';
113
    private const COMPRESSION_KEY = 'compression';
114
    private const DATETIME_KEY = 'datetime';
115
    private const DATETIME_FORMAT_KEY = 'datetime-format';
116
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
117
    private const DIRECTORIES_KEY = 'directories';
118
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
119
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
120
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
121
    private const FILES_KEY = 'files';
122
    private const FILES_BIN_KEY = 'files-bin';
123
    private const FINDER_KEY = 'finder';
124
    private const FINDER_BIN_KEY = 'finder-bin';
125
    private const GIT_KEY = 'git';
126
    private const GIT_COMMIT_KEY = 'git-commit';
127
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
128
    private const GIT_TAG_KEY = 'git-tag';
129
    private const GIT_VERSION_KEY = 'git-version';
130
    private const INTERCEPT_KEY = 'intercept';
131
    private const KEY_KEY = 'key';
132
    private const KEY_PASS_KEY = 'key-pass';
133
    private const MAIN_KEY = 'main';
134
    private const MAP_KEY = 'map';
135
    private const METADATA_KEY = 'metadata';
136
    private const OUTPUT_KEY = 'output';
137
    private const PHP_SCOPER_KEY = 'php-scoper';
138
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
139
    private const REPLACEMENTS_KEY = 'replacements';
140
    private const SHEBANG_KEY = 'shebang';
141
    private const STUB_KEY = 'stub';
142
    
143
    private $file;
144
    private $fileMode;
145
    private $alias;
146
    private $basePath;
147
    private $composerJson;
148
    private $composerLock;
149
    private $files;
150
    private $binaryFiles;
151
    private $autodiscoveredFiles;
152
    private $dumpAutoload;
153
    private $excludeComposerFiles;
154
    private $compactors;
155
    private $compressionAlgorithm;
156
    private $mainScriptPath;
157
    private $mainScriptContents;
158
    private $map;
159
    private $fileMapper;
160
    private $metadata;
161
    private $tmpOutputPath;
162
    private $outputPath;
163
    private $privateKeyPassphrase;
164
    private $privateKeyPath;
165
    private $promptForPrivateKey;
166
    private $processedReplacements;
167
    private $shebang;
168
    private $signingAlgorithm;
169
    private $stubBannerContents;
170
    private $stubBannerPath;
171
    private $stubPath;
172
    private $isInterceptFileFuncs;
173
    private $isStubGenerated;
174
    private $checkRequirements;
175
    private $warnings;
176
    private $recommendations;
177
178
    public static function create(?string $file, stdClass $raw): self
179
    {
180
        $logger = new ConfigurationLogger();
181
182
        $alias = self::retrieveAlias($raw);
183
184
        $basePath = self::retrieveBasePath($file, $raw, $logger);
185
186
        $composerFiles = self::retrieveComposerFiles($basePath);
187
188
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
189
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
190
191
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
192
193
        /** @var (string|null)[] $composerJson */
194
        $composerJson = $composerFiles[0];
195
        /** @var (string|null)[] $composerJson */
196
        $composerLock = $composerFiles[1];
197
198
        $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

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