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

Configuration::getDefaultBanner()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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\Box\Compactor\Json as LegacyJson;
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 Humbug\PhpScoper\Scoper\FileWhitelistScoper;
27
use InvalidArgumentException;
28
use KevinGH\Box\Annotation\AnnotationDumper;
29
use KevinGH\Box\Annotation\DocblockAnnotationParser;
30
use KevinGH\Box\Annotation\DocblockParser;
31
use KevinGH\Box\Compactor\Json as JsonCompactor;
32
use KevinGH\Box\Compactor\Php as PhpCompactor;
33
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
34
use KevinGH\Box\Composer\ComposerConfiguration;
35
use KevinGH\Box\Json\Json;
36
use KevinGH\Box\PhpScoper\SimpleScoper;
37
use Phar;
38
use RuntimeException;
39
use Seld\JsonLint\ParsingException;
40
use SplFileInfo;
41
use stdClass;
42
use Symfony\Component\Finder\Finder;
43
use Symfony\Component\Process\Process;
44
use const E_USER_DEPRECATED;
45
use function array_column;
46
use function array_diff;
47
use function array_filter;
48
use function array_flip;
49
use function array_key_exists;
50
use function array_keys;
51
use function array_map;
52
use function array_merge;
53
use function array_unique;
54
use function array_values;
55
use function array_walk;
56
use function constant;
57
use function current;
58
use function defined;
59
use function dirname;
60
use function explode;
61
use function file_exists;
62
use function getcwd;
63
use function implode;
64
use function in_array;
65
use function intval;
66
use function is_array;
67
use function is_bool;
68
use function is_file;
69
use function is_link;
70
use function is_object;
71
use function is_readable;
72
use function is_string;
73
use function iter\map;
74
use function iter\toArray;
75
use function iter\values;
76
use function KevinGH\Box\FileSystem\canonicalize;
77
use function KevinGH\Box\FileSystem\file_contents;
78
use function KevinGH\Box\FileSystem\is_absolute_path;
79
use function KevinGH\Box\FileSystem\longest_common_base_path;
80
use function KevinGH\Box\FileSystem\make_path_absolute;
81
use function KevinGH\Box\FileSystem\make_path_relative;
82
use function krsort;
83
use function preg_match;
84
use function preg_replace;
85
use function property_exists;
86
use function realpath;
87
use function sprintf;
88
use function strtoupper;
89
use function substr;
90
use function trigger_error;
91
use function trim;
92
93
/**
94
 * @private
95
 */
96
final class Configuration
97
{
98
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
99
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
100
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T';
101
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
102
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
103
    private const DEFAULT_BANNER = <<<'BANNER'
104
Generated by Humbug Box %s.
105
106
@link https://github.com/humbug/box
107
BANNER;
108
    private const FILES_SETTINGS = [
109
        'directories',
110
        'finder',
111
    ];
112
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
113
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
114
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
115
116
    private const DEFAULT_IGNORED_ANNOTATIONS = [
117
        'abstract',
118
        'access',
119
        'annotation',
120
        'api',
121
        'attribute',
122
        'attributes',
123
        'author',
124
        'category',
125
        'code',
126
        'codecoverageignore',
127
        'codecoverageignoreend',
128
        'codecoverageignorestart',
129
        'copyright',
130
        'deprec',
131
        'deprecated',
132
        'endcode',
133
        'example',
134
        'exception',
135
        'filesource',
136
        'final',
137
        'fixme',
138
        'global',
139
        'ignore',
140
        'ingroup',
141
        'inheritdoc',
142
        'internal',
143
        'license',
144
        'link',
145
        'magic',
146
        'method',
147
        'name',
148
        'override',
149
        'package',
150
        'package_version',
151
        'param',
152
        'private',
153
        'property',
154
        'required',
155
        'return',
156
        'see',
157
        'since',
158
        'static',
159
        'staticvar',
160
        'subpackage',
161
        'suppresswarnings',
162
        'target',
163
        'throw',
164
        'throws',
165
        'todo',
166
        'tutorial',
167
        'usedby',
168
        'uses',
169
        'var',
170
        'version',
171
    ];
172
173
    private const ALGORITHM_KEY = 'algorithm';
174
    private const ALIAS_KEY = 'alias';
175
    private const ANNOTATIONS_KEY = 'annotations';
176
    private const IGNORED_ANNOTATIONS_KEY = 'ignore';
177
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
178
    private const BANNER_KEY = 'banner';
179
    private const BANNER_FILE_KEY = 'banner-file';
180
    private const BASE_PATH_KEY = 'base-path';
181
    private const BLACKLIST_KEY = 'blacklist';
182
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
183
    private const CHMOD_KEY = 'chmod';
184
    private const COMPACTORS_KEY = 'compactors';
185
    private const COMPRESSION_KEY = 'compression';
186
    private const DATETIME_KEY = 'datetime';
187
    private const DATETIME_FORMAT_KEY = 'datetime-format';
188
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
189
    private const DIRECTORIES_KEY = 'directories';
190
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
191
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
192
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
193
    private const FILES_KEY = 'files';
194
    private const FILES_BIN_KEY = 'files-bin';
195
    private const FINDER_KEY = 'finder';
196
    private const FINDER_BIN_KEY = 'finder-bin';
197
    private const GIT_KEY = 'git';
198
    private const GIT_COMMIT_KEY = 'git-commit';
199
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
200
    private const GIT_TAG_KEY = 'git-tag';
201
    private const GIT_VERSION_KEY = 'git-version';
202
    private const INTERCEPT_KEY = 'intercept';
203
    private const KEY_KEY = 'key';
204
    private const KEY_PASS_KEY = 'key-pass';
205
    private const MAIN_KEY = 'main';
206
    private const MAP_KEY = 'map';
207
    private const METADATA_KEY = 'metadata';
208
    private const OUTPUT_KEY = 'output';
209
    private const PHP_SCOPER_KEY = 'php-scoper';
210
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
211
    private const REPLACEMENTS_KEY = 'replacements';
212
    private const SHEBANG_KEY = 'shebang';
213
    private const STUB_KEY = 'stub';
214
215
    private $file;
216
    private $fileMode;
217
    private $alias;
218
    private $basePath;
219
    private $composerJson;
220
    private $composerLock;
221
    private $files;
222
    private $binaryFiles;
223
    private $autodiscoveredFiles;
224
    private $dumpAutoload;
225
    private $excludeComposerFiles;
226
    private $compactors;
227
    private $compressionAlgorithm;
228
    private $mainScriptPath;
229
    private $mainScriptContents;
230
    private $fileMapper;
231
    private $metadata;
232
    private $tmpOutputPath;
233
    private $outputPath;
234
    private $privateKeyPassphrase;
235
    private $privateKeyPath;
236
    private $promptForPrivateKey;
237
    private $processedReplacements;
238
    private $shebang;
239
    private $signingAlgorithm;
240
    private $stubBannerContents;
241
    private $stubBannerPath;
242
    private $stubPath;
243
    private $isInterceptFileFuncs;
244
    private $isStubGenerated;
245
    private $checkRequirements;
246
    private $warnings;
247
    private $recommendations;
248
249
    public static function create(?string $file, stdClass $raw): self
250
    {
251
        $logger = new ConfigurationLogger();
252
253
        $basePath = self::retrieveBasePath($file, $raw, $logger);
254
255
        $composerFiles = self::retrieveComposerFiles($basePath);
256
257
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
258
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
259
260
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
261
262
        /** @var (string|null)[] $composerJson */
263
        $composerJson = $composerFiles[0];
264
        /** @var (string|null)[] $composerJson */
265
        $composerLock = $composerFiles[1];
266
267
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
268
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
269
270
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
271
272
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
273
274
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
275
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
276
277
        if (null !== $stubBannerPath) {
278
            $stubBannerContents = file_contents($stubBannerPath);
279
        }
280
281
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
282
283
        if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) {
284
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
285
        }
286
287
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
288
289
        $checkRequirements = self::retrieveCheckRequirements(
290
            $raw,
291
            null !== $composerJson[0],
292
            null !== $composerLock[0],
293
            false === $isStubGenerated && null === $stubPath,
294
            $logger
295
        );
296
297
        $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 array|null, 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

297
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, /** @scrutinizer ignore-type */ $composerJson[1], $composerLock[1]);
Loading history...
298
299
        /**
300
         * @var string[]
301
         * @var Closure  $blacklistFilter
302
         */
303
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter(
304
            $raw,
305
            $basePath,
306
            $logger,
307
            $tmpOutputPath,
308
            $outputPath,
309
            $mainScriptPath
310
        );
311
312
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
313
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger);
314
315
        $filesAggregate = self::collectFiles(
316
            $raw,
317
            $basePath,
318
            $mainScriptPath,
319
            $blacklistFilter,
320
            $excludedPaths,
321
            $devPackages,
322
            $composerFiles,
323
            $composerJson,
324
            $autodiscoverFiles,
325
            $forceFilesAutodiscovery,
326
            $logger
327
        );
328
        $binaryFilesAggregate = self::collectBinaryFiles(
329
            $raw,
330
            $basePath,
331
            $mainScriptPath,
332
            $blacklistFilter,
333
            $excludedPaths,
334
            $devPackages,
335
            $logger
336
        );
337
338
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
339
340
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger);
341
342
        $compactors = self::retrieveCompactors($raw, $basePath, $logger);
343
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger);
344
345
        $fileMode = self::retrieveFileMode($raw, $logger);
346
347
        $map = self::retrieveMap($raw, $logger);
348
        $fileMapper = new MapFile($basePath, $map);
349
350
        $metadata = self::retrieveMetadata($raw, $logger);
351
352
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger);
353
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
354
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
355
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
356
357
        $replacements = self::retrieveReplacements($raw, $file, $logger);
358
359
        return new self(
360
            $file,
361
            $alias,
362
            $basePath,
363
            $composerJson,
364
            $composerLock,
365
            $filesAggregate,
366
            $binaryFilesAggregate,
367
            $autodiscoverFiles || $forceFilesAutodiscovery,
368
            $dumpAutoload,
369
            $excludeComposerFiles,
370
            $compactors,
371
            $compressionAlgorithm,
372
            $fileMode,
373
            $mainScriptPath,
374
            $mainScriptContents,
375
            $fileMapper,
376
            $metadata,
377
            $tmpOutputPath,
378
            $outputPath,
379
            $privateKeyPassphrase,
380
            $privateKeyPath,
381
            $promptForPrivateKey,
382
            $replacements,
383
            $shebang,
384
            $signingAlgorithm,
385
            $stubBannerContents,
386
            $stubBannerPath,
387
            $stubPath,
388
            $isInterceptsFileFuncs,
389
            $isStubGenerated,
390
            $checkRequirements,
391
            $logger->getWarnings(),
392
            $logger->getRecommendations()
393
        );
394
    }
395
396
    /**
397
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
398
     *                                            path relative to it (the base path)
399
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
400
     *                                            string and the second element its decoded contents as an
401
     *                                            associative array.
402
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
403
     *                                            string and the second element its decoded contents as an
404
     *                                            associative array.
405
     * @param SplFileInfo[] $files                List of files
406
     * @param SplFileInfo[] $binaryFiles          List of binary files
407
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
408
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
409
     *                                            installed.json should be removed from the PHAR
410
     * @param Compactor[]   $compactors           List of file contents compactors
411
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
412
     * @param null|int      $fileMode             File mode in octal form
413
     * @param string        $mainScriptPath       The main script file path
414
     * @param string        $mainScriptContents   The processed content of the main script file
415
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
416
     * @param mixed         $metadata             The PHAR Metadata
417
     * @param bool          $promptForPrivateKey  If the user should be prompted for the private key passphrase
418
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
419
     * @param null|string   $shebang              The shebang line
420
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
421
     * @param null|string   $stubBannerContents   The stub banner comment
422
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
423
     * @param null|string   $stubPath             The PHAR stub file path
424
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
425
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
426
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
427
     *                                            running
428
     * @param string[]      $warnings
429
     * @param string[]      $recommendations
430
     */
431
    private function __construct(
432
        ?string $file,
433
        string $alias,
434
        string $basePath,
435
        array $composerJson,
436
        array $composerLock,
437
        array $files,
438
        array $binaryFiles,
439
        bool $autodiscoveredFiles,
440
        bool $dumpAutoload,
441
        bool $excludeComposerFiles,
442
        array $compactors,
443
        ?int $compressionAlgorithm,
444
        ?int $fileMode,
445
        ?string $mainScriptPath,
446
        ?string $mainScriptContents,
447
        MapFile $fileMapper,
448
        $metadata,
449
        string $tmpOutputPath,
450
        string $outputPath,
451
        ?string $privateKeyPassphrase,
452
        ?string $privateKeyPath,
453
        bool $promptForPrivateKey,
454
        array $replacements,
455
        ?string $shebang,
456
        int $signingAlgorithm,
457
        ?string $stubBannerContents,
458
        ?string $stubBannerPath,
459
        ?string $stubPath,
460
        bool $isInterceptFileFuncs,
461
        bool $isStubGenerated,
462
        bool $checkRequirements,
463
        array $warnings,
464
        array $recommendations
465
    ) {
466
        Assertion::nullOrInArray(
467
            $compressionAlgorithm,
468
            get_phar_compression_algorithms(),
469
            sprintf(
470
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
471
                implode('", "', array_keys(get_phar_compression_algorithms()))
472
            )
473
        );
474
475
        if (null === $mainScriptPath) {
476
            Assertion::null($mainScriptContents);
477
        } else {
478
            Assertion::notNull($mainScriptContents);
479
        }
480
481
        $this->file = $file;
482
        $this->alias = $alias;
483
        $this->basePath = $basePath;
484
        $this->composerJson = $composerJson;
485
        $this->composerLock = $composerLock;
486
        $this->files = $files;
487
        $this->binaryFiles = $binaryFiles;
488
        $this->autodiscoveredFiles = $autodiscoveredFiles;
489
        $this->dumpAutoload = $dumpAutoload;
490
        $this->excludeComposerFiles = $excludeComposerFiles;
491
        $this->compactors = $compactors;
492
        $this->compressionAlgorithm = $compressionAlgorithm;
493
        $this->fileMode = $fileMode;
494
        $this->mainScriptPath = $mainScriptPath;
495
        $this->mainScriptContents = $mainScriptContents;
496
        $this->fileMapper = $fileMapper;
497
        $this->metadata = $metadata;
498
        $this->tmpOutputPath = $tmpOutputPath;
499
        $this->outputPath = $outputPath;
500
        $this->privateKeyPassphrase = $privateKeyPassphrase;
501
        $this->privateKeyPath = $privateKeyPath;
502
        $this->promptForPrivateKey = $promptForPrivateKey;
503
        $this->processedReplacements = $replacements;
504
        $this->shebang = $shebang;
505
        $this->signingAlgorithm = $signingAlgorithm;
506
        $this->stubBannerContents = $stubBannerContents;
507
        $this->stubBannerPath = $stubBannerPath;
508
        $this->stubPath = $stubPath;
509
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
510
        $this->isStubGenerated = $isStubGenerated;
511
        $this->checkRequirements = $checkRequirements;
512
        $this->warnings = $warnings;
513
        $this->recommendations = $recommendations;
514
    }
515
516
    public function getConfigurationFile(): ?string
517
    {
518
        return $this->file;
519
    }
520
521
    public function getAlias(): string
522
    {
523
        return $this->alias;
524
    }
525
526
    public function getBasePath(): string
527
    {
528
        return $this->basePath;
529
    }
530
531
    public function getComposerJson(): ?string
532
    {
533
        return $this->composerJson[0];
534
    }
535
536
    public function getDecodedComposerJsonContents(): ?array
537
    {
538
        return $this->composerJson[1];
539
    }
540
541
    public function getComposerLock(): ?string
542
    {
543
        return $this->composerLock[0];
544
    }
545
546
    public function getDecodedComposerLockContents(): ?array
547
    {
548
        return $this->composerLock[1];
549
    }
550
551
    /**
552
     * @return SplFileInfo[]
553
     */
554
    public function getFiles(): array
555
    {
556
        return $this->files;
557
    }
558
559
    /**
560
     * @return SplFileInfo[]
561
     */
562
    public function getBinaryFiles(): array
563
    {
564
        return $this->binaryFiles;
565
    }
566
567
    public function hasAutodiscoveredFiles(): bool
568
    {
569
        return $this->autodiscoveredFiles;
570
    }
571
572
    public function dumpAutoload(): bool
573
    {
574
        return $this->dumpAutoload;
575
    }
576
577
    public function excludeComposerFiles(): bool
578
    {
579
        return $this->excludeComposerFiles;
580
    }
581
582
    /**
583
     * @return Compactor[] the list of compactors
584
     */
585
    public function getCompactors(): array
586
    {
587
        return $this->compactors;
588
    }
589
590
    public function getCompressionAlgorithm(): ?int
591
    {
592
        return $this->compressionAlgorithm;
593
    }
594
595
    public function getFileMode(): ?int
596
    {
597
        return $this->fileMode;
598
    }
599
600
    public function hasMainScript(): bool
601
    {
602
        return null !== $this->mainScriptPath;
603
    }
604
605
    public function getMainScriptPath(): string
606
    {
607
        Assertion::notNull(
608
            $this->mainScriptPath,
609
            'Cannot retrieve the main script path: no main script configured.'
610
        );
611
612
        return $this->mainScriptPath;
613
    }
614
615
    public function getMainScriptContents(): string
616
    {
617
        Assertion::notNull(
618
            $this->mainScriptPath,
619
            'Cannot retrieve the main script contents: no main script configured.'
620
        );
621
622
        return $this->mainScriptContents;
623
    }
624
625
    public function checkRequirements(): bool
626
    {
627
        return $this->checkRequirements;
628
    }
629
630
    public function getTmpOutputPath(): string
631
    {
632
        return $this->tmpOutputPath;
633
    }
634
635
    public function getOutputPath(): string
636
    {
637
        return $this->outputPath;
638
    }
639
640
    public function getFileMapper(): MapFile
641
    {
642
        return $this->fileMapper;
643
    }
644
645
    /**
646
     * @return mixed
647
     */
648
    public function getMetadata()
649
    {
650
        return $this->metadata;
651
    }
652
653
    public function getPrivateKeyPassphrase(): ?string
654
    {
655
        return $this->privateKeyPassphrase;
656
    }
657
658
    public function getPrivateKeyPath(): ?string
659
    {
660
        return $this->privateKeyPath;
661
    }
662
663
    /**
664
     * @deprecated Use promptForPrivateKey() instead
665
     */
666
    public function isPrivateKeyPrompt(): bool
667
    {
668
        return $this->promptForPrivateKey;
669
    }
670
671
    public function promptForPrivateKey(): bool
672
    {
673
        return $this->promptForPrivateKey;
674
    }
675
676
    /**
677
     * @return scalar[]
678
     */
679
    public function getReplacements(): array
680
    {
681
        return $this->processedReplacements;
682
    }
683
684
    public function getShebang(): ?string
685
    {
686
        return $this->shebang;
687
    }
688
689
    public function getSigningAlgorithm(): int
690
    {
691
        return $this->signingAlgorithm;
692
    }
693
694
    public function getStubBannerContents(): ?string
695
    {
696
        return $this->stubBannerContents;
697
    }
698
699
    public function getStubBannerPath(): ?string
700
    {
701
        return $this->stubBannerPath;
702
    }
703
704
    public function getStubPath(): ?string
705
    {
706
        return $this->stubPath;
707
    }
708
709
    public function isInterceptFileFuncs(): bool
710
    {
711
        return $this->isInterceptFileFuncs;
712
    }
713
714
    public function isStubGenerated(): bool
715
    {
716
        return $this->isStubGenerated;
717
    }
718
719
    /**
720
     * @return string[]
721
     */
722
    public function getWarnings(): array
723
    {
724
        return $this->warnings;
725
    }
726
727
    /**
728
     * @return string[]
729
     */
730
    public function getRecommendations(): array
731
    {
732
        return $this->recommendations;
733
    }
734
735
    private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string
736
    {
737
        self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY);
738
739
        if (false === isset($raw->{self::ALIAS_KEY})) {
740
            return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar';
741
        }
742
743
        $alias = trim($raw->{self::ALIAS_KEY});
744
745
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
746
747
        if ($userStubUsed) {
748
            $logger->addWarning(
749
                sprintf(
750
                    'The "%s" setting has been set but is ignored since a custom stub path is used',
751
                    self::ALIAS_KEY
752
                )
753
            );
754
        }
755
756
        return $alias;
757
    }
758
759
    private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string
760
    {
761
        if (null === $file) {
762
            return getcwd();
763
        }
764
765
        if (false === isset($raw->{self::BASE_PATH_KEY})) {
766
            return realpath(dirname($file));
767
        }
768
769
        $basePath = trim($raw->{self::BASE_PATH_KEY});
770
771
        Assertion::directory(
772
            $basePath,
773
            'The base path "%s" is not a directory or does not exist.'
774
        );
775
776
        $basePath = realpath($basePath);
777
        $defaultPath = realpath(dirname($file));
778
779
        if ($basePath === $defaultPath) {
780
            self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY);
781
        }
782
783
        return $basePath;
784
    }
785
786
    /**
787
     * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting.
788
     */
789
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
790
    {
791
        if (null === $file) {
792
            return true;
793
        }
794
795
        $associativeRaw = (array) $raw;
796
797
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw));
798
    }
799
800
    private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool
801
    {
802
        self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false);
803
804
        return $raw->{self::AUTO_DISCOVERY_KEY} ?? false;
805
    }
806
807
    private static function retrieveBlacklistFilter(
808
        stdClass $raw,
809
        string $basePath,
810
        ConfigurationLogger $logger,
811
        ?string ...$excludedPaths
812
    ): array {
813
        $blacklist = array_flip(
814
            self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths)
815
        );
816
817
        $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool {
818
            if ($file->isLink()) {
819
                return false;
820
            }
821
822
            if (false === $file->getRealPath()) {
823
                return false;
824
            }
825
826
            if (array_key_exists($file->getRealPath(), $blacklist)) {
827
                return false;
828
            }
829
830
            return null;
831
        };
832
833
        return [array_keys($blacklist), $blacklistFilter];
834
    }
835
836
    /**
837
     * @param null[]|string[] $excludedPaths
838
     *
839
     * @return string[]
840
     */
841
    private static function retrieveBlacklist(
842
        stdClass $raw,
843
        string $basePath,
844
        ConfigurationLogger $logger,
845
        ?string ...$excludedPaths
846
    ): array {
847
        self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []);
848
849
        $normalizedBlacklist = array_map(
850
            static function (string $excludedPath) use ($basePath): string {
851
                return self::normalizePath($excludedPath, $basePath);
852
            },
853
            array_filter($excludedPaths)
854
        );
855
856
        /** @var string[] $blacklist */
857
        $blacklist = $raw->{self::BLACKLIST_KEY} ?? [];
858
859
        foreach ($blacklist as $file) {
860
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
861
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
862
        }
863
864
        return array_unique($normalizedBlacklist);
865
    }
866
867
    /**
868
     * @param string[] $excludedPaths
869
     * @param string[] $devPackages
870
     *
871
     * @return SplFileInfo[]
872
     */
873
    private static function collectFiles(
874
        stdClass $raw,
875
        string $basePath,
876
        ?string $mainScriptPath,
877
        Closure $blacklistFilter,
878
        array $excludedPaths,
879
        array $devPackages,
880
        array $composerFiles,
881
        array $composerJson,
882
        bool $autodiscoverFiles,
883
        bool $forceFilesAutodiscovery,
884
        ConfigurationLogger $logger
885
    ): array {
886
        $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $mainScriptPath, $logger)];
887
888
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
889
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
890
                $basePath,
891
                $composerJson[1],
892
                $devPackages,
893
                array_filter(
894
                    array_column($composerFiles, 0)
895
                ),
896
                $excludedPaths
897
            );
898
899
            $files[] = $filesToAppend;
900
901
            $files[] = self::retrieveAllFiles(
902
                $basePath,
903
                $directories,
904
                $mainScriptPath,
905
                $blacklistFilter,
906
                $excludedPaths,
907
                $devPackages
908
            );
909
        }
910
911
        if (false === $autodiscoverFiles) {
912
            $files[] = self::retrieveDirectories(
913
                $raw,
914
                self::DIRECTORIES_KEY,
915
                $basePath,
916
                $blacklistFilter,
917
                $excludedPaths,
918
                $logger
919
            );
920
921
            $filesFromFinders = self::retrieveFilesFromFinders(
922
                $raw,
923
                self::FINDER_KEY,
924
                $basePath,
925
                $blacklistFilter,
926
                $devPackages,
927
                $logger
928
            );
929
930
            foreach ($filesFromFinders as $filesFromFinder) {
931
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
932
                $files[] = $filesFromFinder;
933
            }
934
        }
935
936
        return self::retrieveFilesAggregate(...$files);
937
    }
938
939
    /**
940
     * @param string[] $excludedPaths
941
     * @param string[] $devPackages
942
     *
943
     * @return SplFileInfo[]
944
     */
945
    private static function collectBinaryFiles(
946
        stdClass $raw,
947
        string $basePath,
948
        ?string $mainScriptPath,
949
        Closure $blacklistFilter,
950
        array $excludedPaths,
951
        array $devPackages,
952
        ConfigurationLogger $logger
953
    ): array {
954
        $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, [], $mainScriptPath, $logger);
955
956
        $binaryDirectories = self::retrieveDirectories(
957
            $raw,
958
            self::DIRECTORIES_BIN_KEY,
959
            $basePath,
960
            $blacklistFilter,
961
            $excludedPaths,
962
            $logger
963
        );
964
965
        $binaryFilesFromFinders = self::retrieveFilesFromFinders(
966
            $raw,
967
            self::FINDER_BIN_KEY,
968
            $basePath,
969
            $blacklistFilter,
970
            $devPackages,
971
            $logger
972
        );
973
974
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
975
    }
976
977
    /**
978
     * @return SplFileInfo[]
979
     */
980
    private static function retrieveFiles(
981
        stdClass $raw,
982
        string $key,
983
        string $basePath,
984
        array $composerFiles,
985
        ?string $mainScriptPath,
986
        ConfigurationLogger $logger
987
    ): array {
988
        self::checkIfDefaultValue($logger, $raw, $key, []);
989
990
        $files = [];
991
992
        if (isset($composerFiles[0][0])) {
993
            $files[] = $composerFiles[0][0];
994
        }
995
996
        if (isset($composerFiles[1][1])) {
997
            $files[] = $composerFiles[1][0];
998
        }
999
1000
        if (false === isset($raw->{$key})) {
1001
            return $files;
1002
        }
1003
1004
        if ([] === (array) $raw->{$key}) {
1005
            return $files;
1006
        }
1007
1008
        $files = array_merge((array) $raw->{$key}, $files);
1009
1010
        Assertion::allString($files);
1011
1012
        $normalizePath = static function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
1013
            $file = self::normalizePath($file, $basePath);
1014
1015
            Assertion::false(
1016
                is_link($file),
1017
                sprintf(
1018
                    'Cannot add the link "%s": links are not supported.',
1019
                    $file
1020
                )
1021
            );
1022
1023
            Assertion::file(
1024
                $file,
1025
                sprintf(
1026
                    '"%s" must contain a list of existing files. Could not find "%%s".',
1027
                    $key
1028
                )
1029
            );
1030
1031
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
1032
        };
1033
1034
        return array_filter(array_map($normalizePath, $files));
1035
    }
1036
1037
    /**
1038
     * @param string   $key           Config property name
1039
     * @param string[] $excludedPaths
1040
     *
1041
     * @return iterable|SplFileInfo[]
1042
     */
1043
    private static function retrieveDirectories(
1044
        stdClass $raw,
1045
        string $key,
1046
        string $basePath,
1047
        Closure $blacklistFilter,
1048
        array $excludedPaths,
1049
        ConfigurationLogger $logger
1050
    ): iterable {
1051
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger);
1052
1053
        if ([] !== $directories) {
1054
            $finder = Finder::create()
1055
                ->files()
1056
                ->filter($blacklistFilter)
1057
                ->ignoreVCS(true)
1058
                ->in($directories)
1059
            ;
1060
1061
            foreach ($excludedPaths as $excludedPath) {
1062
                $finder->notPath($excludedPath);
1063
            }
1064
1065
            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[]|iterable.
Loading history...
1066
        }
1067
1068
        return [];
1069
    }
1070
1071
    /**
1072
     * @param string[] $devPackages
1073
     *
1074
     * @return iterable[]|SplFileInfo[][]
1075
     */
1076
    private static function retrieveFilesFromFinders(
1077
        stdClass $raw,
1078
        string $key,
1079
        string $basePath,
1080
        Closure $blacklistFilter,
1081
        array $devPackages,
1082
        ConfigurationLogger $logger
1083
    ): array {
1084
        self::checkIfDefaultValue($logger, $raw, $key, []);
1085
1086
        if (false === isset($raw->{$key})) {
1087
            return [];
1088
        }
1089
1090
        $finder = $raw->{$key};
1091
1092
        return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages);
1093
    }
1094
1095
    /**
1096
     * @param iterable[]|SplFileInfo[][] $fileIterators
1097
     *
1098
     * @return SplFileInfo[]
1099
     */
1100
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
1101
    {
1102
        $files = [];
1103
1104
        foreach ($fileIterators as $fileIterator) {
1105
            foreach ($fileIterator as $file) {
1106
                $files[(string) $file] = $file;
1107
            }
1108
        }
1109
1110
        return array_values($files);
1111
    }
1112
1113
    /**
1114
     * @param string[] $devPackages
1115
     *
1116
     * @return Finder[]|SplFileInfo[][]
1117
     */
1118
    private static function processFinders(
1119
        array $findersConfig,
1120
        string $basePath,
1121
        Closure $blacklistFilter,
1122
        array $devPackages
1123
    ): array {
1124
        $processFinderConfig = static function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
1125
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
1126
        };
1127
1128
        return array_map($processFinderConfig, $findersConfig);
1129
    }
1130
1131
    /**
1132
     * @param string[] $devPackages
1133
     *
1134
     * @return Finder|SplFileInfo[]
1135
     */
1136
    private static function processFinder(
1137
        stdClass $config,
1138
        string $basePath,
1139
        Closure $blacklistFilter,
1140
        array $devPackages
1141
    ): Finder {
1142
        $finder = Finder::create()
1143
            ->files()
1144
            ->filter($blacklistFilter)
1145
            ->filter(
1146
                static function (SplFileInfo $fileInfo) use ($devPackages): bool {
1147
                    foreach ($devPackages as $devPackage) {
1148
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
1149
                            // File belongs to the dev package
1150
                            return false;
1151
                        }
1152
                    }
1153
1154
                    return true;
1155
                }
1156
            )
1157
            ->ignoreVCS(true)
1158
        ;
1159
1160
        $normalizedConfig = (static function (array $config, Finder $finder): array {
1161
            $normalizedConfig = [];
1162
1163
            foreach ($config as $method => $arguments) {
1164
                $method = trim($method);
1165
                $arguments = (array) $arguments;
1166
1167
                Assertion::methodExists(
1168
                    $method,
1169
                    $finder,
1170
                    'The method "Finder::%s" does not exist.'
1171
                );
1172
1173
                $normalizedConfig[$method] = $arguments;
1174
            }
1175
1176
            krsort($normalizedConfig);
1177
1178
            return $normalizedConfig;
1179
        })((array) $config, $finder);
1180
1181
        $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string {
1182
            $directory = self::normalizePath($directory, $basePath);
1183
1184
            Assertion::false(
1185
                is_link($directory),
1186
                sprintf(
1187
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1188
                    $directory
1189
                )
1190
            );
1191
1192
            Assertion::directory($directory);
1193
1194
            return $directory;
1195
        };
1196
1197
        $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
1198
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
0 ignored issues
show
Bug introduced by
It seems like $fileOrDirectory can also be of type null; however, parameter $file of KevinGH\Box\Configuration::normalizePath() does only seem to accept string, 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

1198
            $fileOrDirectory = self::normalizePath(/** @scrutinizer ignore-type */ $fileOrDirectory, $basePath);
Loading history...
1199
1200
            Assertion::false(
1201
                is_link($fileOrDirectory),
1202
                sprintf(
1203
                    'Cannot append the link "%s" to the Finder: links are not supported.',
1204
                    $fileOrDirectory
1205
                )
1206
            );
1207
1208
            Assertion::true(
1209
                file_exists($fileOrDirectory),
1210
                sprintf(
1211
                    'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1212
                    $fileOrDirectory
1213
                )
1214
            );
1215
1216
            if (false === is_file($fileOrDirectory)) {
1217
                Assertion::directory($fileOrDirectory);
1218
            } else {
1219
                Assertion::file($fileOrDirectory);
1220
            }
1221
1222
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1223
                $fileOrDirectory = null;
1224
            }
1225
        };
1226
1227
        foreach ($normalizedConfig as $method => $arguments) {
1228
            if ('in' === $method) {
1229
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1230
            }
1231
1232
            if ('exclude' === $method) {
1233
                $arguments = array_unique(array_map('trim', $arguments));
1234
            }
1235
1236
            if ('append' === $method) {
1237
                array_walk($arguments, $normalizeFileOrDirectory);
1238
1239
                $arguments = [array_filter($arguments)];
1240
            }
1241
1242
            foreach ($arguments as $argument) {
1243
                $finder->$method($argument);
1244
            }
1245
        }
1246
1247
        return $finder;
1248
    }
1249
1250
    /**
1251
     * @param string[] $devPackages
1252
     * @param string[] $filesToAppend
1253
     *
1254
     * @return string[][]
1255
     */
1256
    private static function retrieveAllDirectoriesToInclude(
1257
        string $basePath,
1258
        ?array $decodedJsonContents,
1259
        array $devPackages,
1260
        array $filesToAppend,
1261
        array $excludedPaths
1262
    ): array {
1263
        $toString = static function ($file): string {
1264
            // @param string|SplFileInfo $file
1265
            return (string) $file;
1266
        };
1267
1268
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1269
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1270
        } else {
1271
            $vendorDir = self::normalizePath('vendor', $basePath);
1272
        }
1273
1274
        if (file_exists($vendorDir)) {
1275
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1276
            // dependencies are included in the `composer.json`
1277
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1278
1279
            if (file_exists($installedJsonFiles)) {
1280
                $filesToAppend[] = $installedJsonFiles;
1281
            }
1282
1283
            $vendorPackages = toArray(values(map(
1284
                $toString,
1285
                Finder::create()
1286
                    ->in($vendorDir)
1287
                    ->directories()
1288
                    ->depth(1)
1289
                    ->ignoreUnreadableDirs()
1290
                    ->filter(
1291
                        static function (SplFileInfo $fileInfo): ?bool {
1292
                            if ($fileInfo->isLink()) {
1293
                                return false;
1294
                            }
1295
1296
                            return null;
1297
                        }
1298
                    )
1299
            )));
1300
1301
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1302
1303
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1304
                $files = toArray(values(map(
1305
                    $toString,
1306
                    Finder::create()
1307
                        ->in($basePath)
1308
                        ->files()
1309
                        ->depth(0)
1310
                )));
1311
1312
                $directories = toArray(values(map(
1313
                    $toString,
1314
                    Finder::create()
1315
                        ->in($basePath)
1316
                        ->notPath('vendor')
1317
                        ->directories()
1318
                        ->depth(0)
1319
                )));
1320
1321
                return [
1322
                    array_merge($files, $filesToAppend),
1323
                    array_merge($directories, $vendorPackages),
1324
                ];
1325
            }
1326
1327
            $paths = $vendorPackages;
1328
        } else {
1329
            $paths = [];
1330
        }
1331
1332
        $autoload = $decodedJsonContents['autoload'] ?? [];
1333
1334
        if (array_key_exists('psr-4', $autoload)) {
1335
            foreach ($autoload['psr-4'] as $path) {
1336
                /** @var string|string[] $path */
1337
                $composerPaths = (array) $path;
1338
1339
                foreach ($composerPaths as $composerPath) {
1340
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1341
                }
1342
            }
1343
        }
1344
1345
        if (array_key_exists('psr-0', $autoload)) {
1346
            foreach ($autoload['psr-0'] as $path) {
1347
                /** @var string|string[] $path */
1348
                $composerPaths = (array) $path;
1349
1350
                foreach ($composerPaths as $composerPath) {
1351
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1352
                }
1353
            }
1354
        }
1355
1356
        if (array_key_exists('classmap', $autoload)) {
1357
            foreach ($autoload['classmap'] as $path) {
1358
                // @var string $path
1359
                $paths[] = $path;
1360
            }
1361
        }
1362
1363
        $normalizePath = static function (string $path) use ($basePath): string {
1364
            return is_absolute_path($path)
1365
                ? canonicalize($path)
1366
                : self::normalizePath(trim($path, '/ '), $basePath)
1367
            ;
1368
        };
1369
1370
        if (array_key_exists('files', $autoload)) {
1371
            foreach ($autoload['files'] as $path) {
1372
                // @var string $path
1373
                $path = $normalizePath($path);
1374
1375
                Assertion::file($path);
1376
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1377
1378
                $filesToAppend[] = $path;
1379
            }
1380
        }
1381
1382
        $files = $filesToAppend;
1383
        $directories = [];
1384
1385
        foreach ($paths as $path) {
1386
            $path = $normalizePath($path);
1387
1388
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1389
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1390
1391
            if (is_file($path)) {
1392
                $files[] = $path;
1393
            } else {
1394
                $directories[] = $path;
1395
            }
1396
        }
1397
1398
        [$files, $directories] = [
1399
            array_unique($files),
1400
            array_unique($directories),
1401
        ];
1402
1403
        return [
1404
            array_diff($files, $excludedPaths),
1405
            array_diff($directories, $excludedPaths),
1406
        ];
1407
    }
1408
1409
    /**
1410
     * @param string[] $files
1411
     * @param string[] $directories
1412
     * @param string[] $excludedPaths
1413
     * @param string[] $devPackages
1414
     *
1415
     * @return SplFileInfo[]
1416
     */
1417
    private static function retrieveAllFiles(
1418
        string $basePath,
1419
        array $directories,
1420
        ?string $mainScriptPath,
1421
        Closure $blacklistFilter,
1422
        array $excludedPaths,
1423
        array $devPackages
1424
    ): iterable {
1425
        if ([] === $directories) {
1426
            return [];
1427
        }
1428
1429
        $relativeDevPackages = array_map(
1430
            static function (string $packagePath) use ($basePath): string {
1431
                return make_path_relative($packagePath, $basePath);
1432
            },
1433
            $devPackages
1434
        );
1435
1436
        $finder = Finder::create()
1437
            ->files()
1438
            ->filter($blacklistFilter)
1439
            ->exclude($relativeDevPackages)
1440
            ->ignoreVCS(true)
1441
            ->ignoreDotFiles(true)
1442
            // Remove build files
1443
            ->notName('composer.json')
1444
            ->notName('composer.lock')
1445
            ->notName('Makefile')
1446
            ->notName('Vagrantfile')
1447
            ->notName('phpstan*.neon*')
1448
            ->notName('infection*.json*')
1449
            ->notName('humbug*.json*')
1450
            ->notName('easy-coding-standard.neon*')
1451
            ->notName('phpbench.json*')
1452
            ->notName('phpcs.xml*')
1453
            ->notName('psalm.xml*')
1454
            ->notName('scoper.inc*')
1455
            ->notName('box*.json*')
1456
            ->notName('phpdoc*.xml*')
1457
            ->notName('codecov.yml*')
1458
            ->notName('Dockerfile')
1459
            ->exclude('build')
1460
            ->exclude('dist')
1461
            ->exclude('example')
1462
            ->exclude('examples')
1463
            // Remove documentation
1464
            ->notName('*.md')
1465
            ->notName('*.rst')
1466
            ->notName('/^readme(\..*+)?$/i')
1467
            ->notName('/^upgrade(\..*+)?$/i')
1468
            ->notName('/^contributing(\..*+)?$/i')
1469
            ->notName('/^changelog(\..*+)?$/i')
1470
            ->notName('/^authors?(\..*+)?$/i')
1471
            ->notName('/^conduct(\..*+)?$/i')
1472
            ->notName('/^todo(\..*+)?$/i')
1473
            ->exclude('doc')
1474
            ->exclude('docs')
1475
            ->exclude('documentation')
1476
            // Remove backup files
1477
            ->notName('*~')
1478
            ->notName('*.back')
1479
            ->notName('*.swp')
1480
            // Remove tests
1481
            ->notName('*Test.php')
1482
            ->exclude('test')
1483
            ->exclude('Test')
1484
            ->exclude('tests')
1485
            ->exclude('Tests')
1486
            ->notName('/phpunit.*\.xml(.dist)?/')
1487
            ->notName('/behat.*\.yml(.dist)?/')
1488
            ->exclude('spec')
1489
            ->exclude('specs')
1490
            ->exclude('features')
1491
            // Remove CI config
1492
            ->exclude('travis')
1493
            ->notName('travis.yml')
1494
            ->notName('appveyor.yml')
1495
            ->notName('build.xml*')
1496
        ;
1497
1498
        if (null !== $mainScriptPath) {
1499
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1500
        }
1501
1502
        $finder->in($directories);
1503
1504
        $excludedPaths = array_unique(
1505
            array_filter(
1506
                array_map(
1507
                    static function (string $path) use ($basePath): string {
1508
                        return make_path_relative($path, $basePath);
1509
                    },
1510
                    $excludedPaths
1511
                ),
1512
                static function (string $path): bool {
1513
                    return '..' !== substr($path, 0, 2);
1514
                }
1515
            )
1516
        );
1517
1518
        foreach ($excludedPaths as $excludedPath) {
1519
            $finder->notPath($excludedPath);
1520
        }
1521
1522
        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...
1523
    }
1524
1525
    /**
1526
     * @param string $key Config property name
1527
     *
1528
     * @return string[]
1529
     */
1530
    private static function retrieveDirectoryPaths(
1531
        stdClass $raw,
1532
        string $key,
1533
        string $basePath,
1534
        ConfigurationLogger $logger
1535
    ): array {
1536
        self::checkIfDefaultValue($logger, $raw, $key, []);
1537
1538
        if (false === isset($raw->{$key})) {
1539
            return [];
1540
        }
1541
1542
        $directories = $raw->{$key};
1543
1544
        $normalizeDirectory = static function (string $directory) use ($basePath, $key): string {
1545
            $directory = self::normalizePath($directory, $basePath);
1546
1547
            Assertion::false(
1548
                is_link($directory),
1549
                sprintf(
1550
                    'Cannot add the link "%s": links are not supported.',
1551
                    $directory
1552
                )
1553
            );
1554
1555
            Assertion::directory(
1556
                $directory,
1557
                sprintf(
1558
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1559
                    $key
1560
                )
1561
            );
1562
1563
            return $directory;
1564
        };
1565
1566
        return array_map($normalizeDirectory, $directories);
1567
    }
1568
1569
    private static function normalizePath(string $file, string $basePath): string
1570
    {
1571
        return make_path_absolute(trim($file), $basePath);
1572
    }
1573
1574
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, ConfigurationLogger $logger): bool
1575
    {
1576
        self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true);
1577
1578
        if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) {
1579
            return $composerJson;
1580
        }
1581
1582
        $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true;
1583
1584
        if (false === $composerJson && $dumpAutoload) {
1585
            $logger->addWarning(
1586
                'The "dump-autoload" setting has been set but has been ignored because the composer.json file necessary'
1587
                .' for it could not be found'
1588
            );
1589
1590
            return false;
1591
        }
1592
1593
        return $composerJson && false !== $dumpAutoload;
1594
    }
1595
1596
    private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool
1597
    {
1598
        self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true);
1599
1600
        return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true;
1601
    }
1602
1603
    /**
1604
     * @return Compactor[]
1605
     */
1606
    private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): array
1607
    {
1608
        self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []);
1609
1610
        $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? []));
1611
1612
        $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger);
1613
1614
        if (false === isset($raw->{self::COMPACTORS_KEY})) {
1615
            return [];
1616
        }
1617
1618
        $compactors = array_map(
1619
            static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor {
1620
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1621
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1622
1623
                if (LegacyPhp::class === $class) {
1624
                    $logger->addRecommendation(
1625
                        sprintf(
1626
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1627
                            LegacyPhp::class,
1628
                            PhpCompactor::class
1629
                        )
1630
                    );
1631
                }
1632
1633
                if (LegacyJson::class === $class) {
1634
                    $logger->addRecommendation(
1635
                        sprintf(
1636
                            'The compactor "%s" has been deprecated, use "%s" instead.',
1637
                            LegacyJson::class,
1638
                            JsonCompactor::class
1639
                        )
1640
                    );
1641
                }
1642
1643
                if (PhpCompactor::class === $class || LegacyPhp::class === $class) {
1644
                    return self::createPhpCompactor($ignoredAnnotations);
1645
                }
1646
1647
                if (PhpScoperCompactor::class === $class) {
1648
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1649
                }
1650
1651
                return new $class();
1652
            },
1653
            $compactorClasses
1654
        );
1655
1656
        $scoperCompactor = false;
1657
1658
        foreach ($compactors as $compactor) {
1659
            if ($compactor instanceof PhpScoperCompactor) {
1660
                $scoperCompactor = true;
1661
            }
1662
1663
            if ($compactor instanceof PhpCompactor) {
1664
                if (true === $scoperCompactor) {
1665
                    $logger->addRecommendation(
1666
                        sprintf(
1667
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1668
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1669
                        )
1670
                    );
1671
                }
1672
1673
                break;
1674
            }
1675
        }
1676
1677
        return $compactors;
1678
    }
1679
1680
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1681
    {
1682
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1683
1684
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1685
            return null;
1686
        }
1687
1688
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1689
1690
        Assertion::inArray(
1691
            $raw->{self::COMPRESSION_KEY},
1692
            $knownAlgorithmNames,
1693
            sprintf(
1694
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1695
                implode('", "', $knownAlgorithmNames)
1696
            )
1697
        );
1698
1699
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1700
1701
        // Phar::NONE is not valid for compressFiles()
1702
        if (Phar::NONE === $value) {
1703
            return null;
1704
        }
1705
1706
        return $value;
1707
    }
1708
1709
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1710
    {
1711
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1712
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1713
        }
1714
1715
        $defaultChmod = intval(0755, 8);
1716
1717
        if (isset($raw->{self::CHMOD_KEY})) {
1718
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1719
1720
            if ($defaultChmod === $chmod) {
1721
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1722
            }
1723
1724
            return $chmod;
1725
        }
1726
1727
        return $defaultChmod;
1728
    }
1729
1730
    private static function retrieveMainScriptPath(
1731
        stdClass $raw,
1732
        string $basePath,
1733
        ?array $decodedJsonContents,
1734
        ConfigurationLogger $logger
1735
    ): ?string {
1736
        $firstBin = false;
1737
1738
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1739
            /** @var false|string $firstBin */
1740
            $firstBin = current((array) $decodedJsonContents['bin']);
1741
1742
            if (false !== $firstBin) {
1743
                $firstBin = self::normalizePath($firstBin, $basePath);
1744
            }
1745
        }
1746
1747
        if (isset($raw->{self::MAIN_KEY})) {
1748
            $main = $raw->{self::MAIN_KEY};
1749
1750
            if (is_string($main)) {
1751
                $main = self::normalizePath($main, $basePath);
1752
1753
                if ($main === $firstBin) {
1754
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1755
                }
1756
            }
1757
        } else {
1758
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1759
        }
1760
1761
        if (is_bool($main)) {
1762
            Assertion::false(
1763
                $main,
1764
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1765
            );
1766
1767
            return null;
1768
        }
1769
1770
        Assertion::file($main);
1771
1772
        return $main;
1773
    }
1774
1775
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1776
    {
1777
        if (null === $mainScriptPath) {
1778
            return null;
1779
        }
1780
1781
        $contents = file_contents($mainScriptPath);
1782
1783
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1784
        // PHAR entry point file.
1785
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1786
        // case the main script can be ignored or disabled.
1787
        return preg_replace('/^#!.*\s*/', '', $contents);
1788
    }
1789
1790
    /**
1791
     * @return string|null[][]
1792
     */
1793
    private static function retrieveComposerFiles(string $basePath): array
1794
    {
1795
        $retrieveFileAndContents = static function (string $file): array {
1796
            $json = new Json();
1797
1798
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1799
                return [null, null];
1800
            }
1801
1802
            try {
1803
                $contents = $json->decodeFile($file, true);
1804
            } catch (ParsingException $exception) {
1805
                throw new InvalidArgumentException(
1806
                    sprintf(
1807
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1808
                        $file,
1809
                        $exception->getMessage()
1810
                    ),
1811
                    0,
1812
                    $exception
1813
                );
1814
            }
1815
1816
            return [$file, $contents];
1817
        };
1818
1819
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1820
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1821
1822
        return [
1823
            [$composerJson, $composerJsonContents],
1824
            [$composerLock, $composerLockContents],
1825
        ];
1826
    }
1827
1828
    /**
1829
     * @return string[][]
1830
     */
1831
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1832
    {
1833
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1834
1835
        if (false === isset($raw->{self::MAP_KEY})) {
1836
            return [];
1837
        }
1838
1839
        $map = [];
1840
        $rawMap = (array) $raw->{self::MAP_KEY};
1841
1842
        foreach ($rawMap as $item) {
1843
            $processed = [];
1844
1845
            foreach ($item as $match => $replace) {
1846
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1847
            }
1848
1849
            if (isset($processed['_empty_'])) {
1850
                $processed[''] = $processed['_empty_'];
1851
1852
                unset($processed['_empty_']);
1853
            }
1854
1855
            $map[] = $processed;
1856
        }
1857
1858
        return $map;
1859
    }
1860
1861
    /**
1862
     * @return mixed
1863
     */
1864
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1865
    {
1866
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1867
1868
        if (false === isset($raw->{self::METADATA_KEY})) {
1869
            return null;
1870
        }
1871
1872
        $metadata = $raw->{self::METADATA_KEY};
1873
1874
        return is_object($metadata) ? (array) $metadata : $metadata;
1875
    }
1876
1877
    /**
1878
     * @return string[] The first element is the temporary output path and the second the final one
1879
     */
1880
    private static function retrieveOutputPath(
1881
        stdClass $raw,
1882
        string $basePath,
1883
        ?string $mainScriptPath,
1884
        ConfigurationLogger $logger
1885
    ): array {
1886
        $defaultPath = null;
1887
1888
        if (null !== $mainScriptPath
1889
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
1890
        ) {
1891
            $defaultPath = $matches['main'].'.phar';
1892
        }
1893
1894
        if (isset($raw->{self::OUTPUT_KEY})) {
1895
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1896
1897
            if ($path === $defaultPath) {
1898
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1899
            }
1900
        } elseif (null !== $defaultPath) {
1901
            $path = $defaultPath;
1902
        } else {
1903
            // Last resort, should not happen
1904
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1905
        }
1906
1907
        $tmp = $real = $path;
1908
1909
        if ('.phar' !== substr($real, -5)) {
1910
            $tmp .= '.phar';
1911
        }
1912
1913
        return [$tmp, $real];
1914
    }
1915
1916
    private static function retrievePrivateKeyPath(
1917
        stdClass $raw,
1918
        string $basePath,
1919
        int $signingAlgorithm,
1920
        ConfigurationLogger $logger
1921
    ): ?string {
1922
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1923
            if (null === $raw->{self::KEY_KEY}) {
1924
                $logger->addRecommendation(
1925
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1926
                );
1927
            } else {
1928
                $logger->addWarning(
1929
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1930
                );
1931
            }
1932
1933
            return null;
1934
        }
1935
1936
        if (!isset($raw->{self::KEY_KEY})) {
1937
            Assertion::true(
1938
                Phar::OPENSSL !== $signingAlgorithm,
1939
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1940
            );
1941
1942
            return null;
1943
        }
1944
1945
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1946
1947
        Assertion::file($path);
1948
1949
        return $path;
1950
    }
1951
1952
    private static function retrievePrivateKeyPassphrase(
1953
        stdClass $raw,
1954
        int $algorithm,
1955
        ConfigurationLogger $logger
1956
    ): ?string {
1957
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1958
1959
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1960
            return null;
1961
        }
1962
1963
        /** @var null|false|string $keyPass */
1964
        $keyPass = $raw->{self::KEY_PASS_KEY};
1965
1966
        if (Phar::OPENSSL !== $algorithm) {
1967
            if (false === $keyPass || null === $keyPass) {
1968
                $logger->addRecommendation(
1969
                    sprintf(
1970
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1971
                        .'not "OPENSSL".',
1972
                        self::KEY_PASS_KEY
1973
                    )
1974
                );
1975
            } else {
1976
                $logger->addWarning(
1977
                    sprintf(
1978
                        'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1979
                        self::KEY_PASS_KEY
1980
                    )
1981
                );
1982
            }
1983
1984
            return null;
1985
        }
1986
1987
        return is_string($keyPass) ? $keyPass : null;
1988
    }
1989
1990
    /**
1991
     * @return scalar[]
1992
     */
1993
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1994
    {
1995
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1996
1997
        if (null === $file) {
1998
            return [];
1999
        }
2000
2001
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
2002
2003
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
2004
            $replacements[$git] = self::retrievePrettyGitTag($file);
2005
        }
2006
2007
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
2008
            $replacements[$git] = self::retrieveGitHash($file);
2009
        }
2010
2011
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
2012
            $replacements[$git] = self::retrieveGitHash($file, true);
2013
        }
2014
2015
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
2016
            $replacements[$git] = self::retrieveGitTag($file);
2017
        }
2018
2019
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
2020
            $replacements[$git] = self::retrieveGitVersion($file);
2021
        }
2022
2023
        /**
2024
         * @var string
2025
         * @var bool   $valueSetByUser
2026
         */
2027
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
2028
2029
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
2030
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
2031
        } elseif ($valueSetByUser) {
2032
            $logger->addRecommendation(
2033
                sprintf(
2034
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
2035
                    self::DATETIME_FORMAT_KEY,
2036
                    self::DATETIME_KEY
2037
                )
2038
            );
2039
        }
2040
2041
        $sigil = self::retrieveReplacementSigil($raw, $logger);
2042
2043
        foreach ($replacements as $key => $value) {
2044
            unset($replacements[$key]);
2045
            $replacements[$sigil.$key.$sigil] = $value;
2046
        }
2047
2048
        return $replacements;
2049
    }
2050
2051
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2052
    {
2053
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
2054
    }
2055
2056
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2057
    {
2058
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
2059
    }
2060
2061
    /**
2062
     * @param bool $short Use the short version
2063
     *
2064
     * @return string the commit hash
2065
     */
2066
    private static function retrieveGitHash(string $file, bool $short = false): string
2067
    {
2068
        return self::runGitCommand(
2069
            sprintf(
2070
                'git log --pretty="%s" -n1 HEAD',
2071
                $short ? '%h' : '%H'
2072
            ),
2073
            $file
2074
        );
2075
    }
2076
2077
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2078
    {
2079
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
2080
    }
2081
2082
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2083
    {
2084
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
2085
    }
2086
2087
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
2088
    {
2089
        self::checkIfDefaultValue($logger, $raw, $key);
2090
2091
        return $raw->{$key} ?? null;
2092
    }
2093
2094
    private static function retrieveGitTag(string $file): string
2095
    {
2096
        return self::runGitCommand('git describe --tags HEAD', $file);
2097
    }
2098
2099
    private static function retrievePrettyGitTag(string $file): string
2100
    {
2101
        $version = self::retrieveGitTag($file);
2102
2103
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2104
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2105
        }
2106
2107
        return $version;
2108
    }
2109
2110
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2111
    {
2112
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2113
    }
2114
2115
    private static function retrieveGitVersion(string $file): ?string
2116
    {
2117
        try {
2118
            return self::retrieveGitTag($file);
2119
        } catch (RuntimeException $exception) {
2120
            try {
2121
                return self::retrieveGitHash($file, true);
2122
            } catch (RuntimeException $exception) {
2123
                throw new RuntimeException(
2124
                    sprintf(
2125
                        'The tag or commit hash could not be retrieved from "%s": %s',
2126
                        dirname($file),
2127
                        $exception->getMessage()
2128
                    ),
2129
                    0,
2130
                    $exception
2131
                );
2132
            }
2133
        }
2134
    }
2135
2136
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2137
    {
2138
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2139
    }
2140
2141
    private static function retrieveDatetimeNow(string $format): string
2142
    {
2143
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2144
2145
        return $now->format($format);
2146
    }
2147
2148
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2149
    {
2150
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2151
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2152
2153
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2154
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2155
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2156
            @trigger_error(
2157
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2158
                E_USER_DEPRECATED
2159
            );
2160
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2161
2162
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2163
        } else {
2164
            $format = null;
2165
        }
2166
2167
        if (null !== $format) {
2168
            $formattedDate = (new DateTimeImmutable())->format($format);
2169
2170
            Assertion::false(
2171
                false === $formattedDate || $formattedDate === $format,
2172
                sprintf(
2173
                    'Expected the datetime format to be a valid format: "%s" is not',
2174
                    $format
2175
                )
2176
            );
2177
2178
            return [$format, true];
2179
        }
2180
2181
        return [self::DEFAULT_DATETIME_FORMAT, false];
2182
    }
2183
2184
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2185
    {
2186
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2187
    }
2188
2189
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2190
    {
2191
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2192
2193
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2194
            return self::DEFAULT_SHEBANG;
2195
        }
2196
2197
        $shebang = $raw->{self::SHEBANG_KEY};
2198
2199
        if (false === $shebang) {
2200
            if (false === $stubIsGenerated) {
2201
                $logger->addRecommendation(
2202
                    sprintf(
2203
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2204
                        .' being used',
2205
                        self::SHEBANG_KEY
2206
                    )
2207
                );
2208
            }
2209
2210
            return null;
2211
        }
2212
2213
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2214
2215
        $shebang = trim($shebang);
2216
2217
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2218
        Assertion::true(
2219
            '#!' === substr($shebang, 0, 2),
2220
            sprintf(
2221
                'The shebang line must start with "#!". Got "%s" instead',
2222
                $shebang
2223
            )
2224
        );
2225
2226
        if (false === $stubIsGenerated) {
2227
            $logger->addWarning(
2228
                sprintf(
2229
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2230
                    .' used',
2231
                    self::SHEBANG_KEY
2232
                )
2233
            );
2234
        }
2235
2236
        return $shebang;
2237
    }
2238
2239
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2240
    {
2241
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2242
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2243
        }
2244
2245
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2246
            return self::DEFAULT_SIGNING_ALGORITHM;
2247
        }
2248
2249
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2250
2251
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2252
2253
        Assertion::true(
2254
            defined('Phar::'.$algorithm),
2255
            sprintf(
2256
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2257
                $algorithm
2258
            )
2259
        );
2260
2261
        $algorithm = constant('Phar::'.$algorithm);
2262
2263
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2264
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2265
        }
2266
2267
        return $algorithm;
2268
    }
2269
2270
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2271
    {
2272
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner());
2273
2274
        if (false === isset($raw->{self::BANNER_KEY})) {
2275
            return self::getDefaultBanner();
2276
        }
2277
2278
        $banner = $raw->{self::BANNER_KEY};
2279
2280
        if (false === $banner) {
2281
            if (false === $stubIsGenerated) {
2282
                $logger->addRecommendation(
2283
                    sprintf(
2284
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2285
                        .'being used',
2286
                        self::BANNER_KEY
2287
                    )
2288
                );
2289
            }
2290
2291
            return null;
2292
        }
2293
2294
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2295
2296
        if (is_array($banner)) {
2297
            $banner = implode("\n", $banner);
2298
        }
2299
2300
        if (false === $stubIsGenerated) {
2301
            $logger->addWarning(
2302
                sprintf(
2303
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2304
                    self::BANNER_KEY
2305
                )
2306
            );
2307
        }
2308
2309
        return $banner;
2310
    }
2311
2312
    private static function getDefaultBanner(): string
2313
    {
2314
        return sprintf(self::DEFAULT_BANNER, get_box_version());
2315
    }
2316
2317
    private static function retrieveStubBannerPath(
2318
        stdClass $raw,
2319
        string $basePath,
2320
        bool $stubIsGenerated,
2321
        ConfigurationLogger $logger
2322
    ): ?string {
2323
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2324
2325
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2326
            return null;
2327
        }
2328
2329
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2330
2331
        Assertion::file($bannerFile);
2332
2333
        if (false === $stubIsGenerated) {
2334
            $logger->addWarning(
2335
                sprintf(
2336
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2337
                    self::BANNER_FILE_KEY
2338
                )
2339
            );
2340
        }
2341
2342
        return $bannerFile;
2343
    }
2344
2345
    private static function normalizeStubBannerContents(?string $contents): ?string
2346
    {
2347
        if (null === $contents) {
2348
            return null;
2349
        }
2350
2351
        $banner = explode("\n", $contents);
2352
        $banner = array_map('trim', $banner);
2353
2354
        return implode("\n", $banner);
2355
    }
2356
2357
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2358
    {
2359
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2360
2361
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2362
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2363
2364
            Assertion::file($stubPath);
2365
2366
            return $stubPath;
2367
        }
2368
2369
        return null;
2370
    }
2371
2372
    private static function retrieveInterceptsFileFuncs(
2373
        stdClass $raw,
2374
        bool $stubIsGenerated,
2375
        ConfigurationLogger $logger
2376
    ): bool {
2377
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2378
2379
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2380
            return false;
2381
        }
2382
2383
        $intercept = $raw->{self::INTERCEPT_KEY};
2384
2385
        if ($intercept && false === $stubIsGenerated) {
2386
            $logger->addWarning(
2387
                sprintf(
2388
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2389
                    self::INTERCEPT_KEY
2390
                )
2391
            );
2392
        }
2393
2394
        return $intercept;
2395
    }
2396
2397
    private static function retrievePromptForPrivateKey(
2398
        stdClass $raw,
2399
        int $signingAlgorithm,
2400
        ConfigurationLogger $logger
2401
    ): bool {
2402
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2403
            if (Phar::OPENSSL !== $signingAlgorithm) {
2404
                $logger->addWarning(
2405
                    'A prompt for password for the private key has been requested but ignored since the signing '
2406
                    .'algorithm used is not "OPENSSL.'
2407
                );
2408
2409
                return false;
2410
            }
2411
2412
            return true;
2413
        }
2414
2415
        return false;
2416
    }
2417
2418
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2419
    {
2420
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2421
2422
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2423
    }
2424
2425
    private static function retrieveCheckRequirements(
2426
        stdClass $raw,
2427
        bool $hasComposerJson,
2428
        bool $hasComposerLock,
2429
        bool $pharStubUsed,
2430
        ConfigurationLogger $logger
2431
    ): bool {
2432
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2433
2434
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2435
            return $hasComposerJson || $hasComposerLock;
2436
        }
2437
2438
        /** @var bool $checkRequirements */
2439
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2440
2441
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2442
            $logger->addWarning(
2443
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2444
                .'be found.'
2445
            );
2446
2447
            return false;
2448
        }
2449
2450
        if ($checkRequirements && $pharStubUsed) {
2451
            $logger->addWarning(
2452
                sprintf(
2453
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2454
                    .'used.',
2455
                    self::CHECK_REQUIREMENTS_KEY
2456
                )
2457
            );
2458
        }
2459
2460
        return $checkRequirements;
2461
    }
2462
2463
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2464
    {
2465
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2466
2467
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2468
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2469
2470
            return file_exists($configFilePath)
2471
                ? PhpScoperConfiguration::load($configFilePath)
2472
                : PhpScoperConfiguration::load()
2473
             ;
2474
        }
2475
2476
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2477
2478
        Assertion::string($configFile);
2479
2480
        $configFilePath = make_path_absolute($configFile, $basePath);
2481
2482
        Assertion::file($configFilePath);
2483
        Assertion::readable($configFilePath);
2484
2485
        return PhpScoperConfiguration::load($configFilePath);
2486
    }
2487
2488
    /**
2489
     * Runs a Git command on the repository.
2490
     *
2491
     * @param string $command the command
2492
     *
2493
     * @return string the trimmed output from the command
2494
     */
2495
    private static function runGitCommand(string $command, string $file): string
2496
    {
2497
        $path = dirname($file);
2498
2499
        $process = new Process($command, $path);
2500
2501
        if (0 === $process->run()) {
2502
            return trim($process->getOutput());
2503
        }
2504
2505
        throw new RuntimeException(
2506
            sprintf(
2507
                'The tag or commit hash could not be retrieved from "%s": %s',
2508
                $path,
2509
                $process->getErrorOutput()
2510
            )
2511
        );
2512
    }
2513
2514
    /**
2515
     * @param string[] $compactorClasses
2516
     *
2517
     * @return string[]
2518
     */
2519
    private static function retrievePhpCompactorIgnoredAnnotations(
2520
        stdClass $raw,
2521
        array $compactorClasses,
2522
        ConfigurationLogger $logger
2523
    ): array {
2524
        $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true) || in_array(LegacyPhp::class, $compactorClasses, true);
2525
2526
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true);
2527
        self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null);
2528
2529
        if (false === property_exists($raw, self::ANNOTATIONS_KEY)) {
2530
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2531
        }
2532
2533
        if (false === $hasPhpCompactor) {
2534
            $logger->addWarning(
2535
                sprintf(
2536
                    'The "%s" setting has been set but is ignored since no PHP compactor has been configured',
2537
                    self::ANNOTATIONS_KEY
2538
                )
2539
            );
2540
        }
2541
2542
        /** @var null|bool|stdClass $annotations */
2543
        $annotations = $raw->{self::ANNOTATIONS_KEY};
2544
2545
        if (true === $annotations || null === $annotations) {
2546
            return self::DEFAULT_IGNORED_ANNOTATIONS;
2547
        }
2548
2549
        if (false === $annotations) {
0 ignored issues
show
introduced by
The condition false === $annotations is always true.
Loading history...
2550
            return [];
2551
        }
2552
2553
        if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) {
2554
            $logger->addWarning(
2555
                sprintf(
2556
                    'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as'
2557
                    .' if it is set to `false`',
2558
                    self::ANNOTATIONS_KEY,
2559
                    self::IGNORED_ANNOTATIONS_KEY,
2560
                    self::ANNOTATIONS_KEY
2561
                )
2562
            );
2563
2564
            return [];
2565
        }
2566
2567
        $ignored = [];
2568
2569
        if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)
2570
            && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true)
2571
        ) {
2572
            self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY);
2573
2574
            return (array) $ignored;
2575
        }
2576
2577
        return $ignored;
2578
    }
2579
2580
    private static function createPhpCompactor(array $ignoredAnnotations): Compactor
2581
    {
2582
        $ignoredAnnotations = array_values(
2583
            array_filter(
2584
                array_map(
2585
                    static function (string $annotation): ?string {
2586
                        return strtolower(trim($annotation));
2587
                    },
2588
                    $ignoredAnnotations
2589
                )
2590
            )
2591
        );
2592
2593
        return new PhpCompactor(
2594
            new DocblockAnnotationParser(
2595
                new DocblockParser(),
2596
                new AnnotationDumper(),
2597
                $ignoredAnnotations
2598
            )
2599
        );
2600
    }
2601
2602
    private static function createPhpScoperCompactor(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactor
2603
    {
2604
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2605
2606
        $phpScoper = (new class() extends ApplicationFactory {
2607
            public static function createScoper(): Scoper
2608
            {
2609
                return parent::createScoper();
2610
            }
2611
        })::createScoper();
2612
2613
        if ([] !== $phpScoperConfig->getWhitelistedFiles()) {
2614
            $whitelistedFiles = array_values(
2615
                array_unique(
2616
                    array_map(
2617
                        static function (string $path) use ($basePath): string {
2618
                            return make_path_relative($path, $basePath);
2619
                        },
2620
                        $phpScoperConfig->getWhitelistedFiles()
2621
                    )
2622
                )
2623
            );
2624
2625
            $phpScoper = new FileWhitelistScoper($phpScoper, ...$whitelistedFiles);
2626
        }
2627
2628
        $prefix = null === $phpScoperConfig->getPrefix()
2629
            ? unique_id('_HumbugBox')
2630
            : $phpScoperConfig->getPrefix()
2631
        ;
2632
2633
        return new PhpScoperCompactor(
2634
            new SimpleScoper(
2635
                $phpScoper,
2636
                $prefix,
2637
                $phpScoperConfig->getWhitelist(),
2638
                $phpScoperConfig->getPatchers()
2639
            )
2640
        );
2641
    }
2642
2643
    private static function checkIfDefaultValue(
2644
        ConfigurationLogger $logger,
2645
        stdClass $raw,
2646
        string $key,
2647
        $defaultValue = null
2648
    ): void {
2649
        if (false === property_exists($raw, $key)) {
2650
            return;
2651
        }
2652
2653
        $value = $raw->{$key};
2654
2655
        if (null === $value
2656
            || (false === is_object($defaultValue) && $defaultValue === $value)
2657
            || (is_object($defaultValue) && $defaultValue == $value)
2658
        ) {
2659
            $logger->addRecommendation(
2660
                sprintf(
2661
                    'The "%s" setting can be omitted since is set to its default value',
2662
                    $key
2663
                )
2664
            );
2665
        }
2666
    }
2667
2668
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2669
    {
2670
        $logger->addRecommendation(
2671
            sprintf(
2672
                'The "%s" setting can be omitted since is set to its default value',
2673
                $key
2674
            )
2675
        );
2676
    }
2677
}
2678