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

Configuration::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 83
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Herrera\Annotations\Tokenizer;
22
use Herrera\Box\Compactor\Php as LegacyPhp;
23
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
24
use Humbug\PhpScoper\Console\ApplicationFactory;
25
use Humbug\PhpScoper\Scoper;
26
use InvalidArgumentException;
27
use KevinGH\Box\Compactor\Php;
28
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
29
use KevinGH\Box\Composer\ComposerConfiguration;
30
use KevinGH\Box\Json\Json;
31
use KevinGH\Box\PhpScoper\SimpleScoper;
32
use Phar;
33
use 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