Passed
Pull Request — master (#315)
by Théo
06:13 queued 01:44
created

Configuration.php$0 ➔ createScoper()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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