Completed
Pull Request — master (#31)
by Théo
02:27
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 71
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 71
rs 9.1369
c 0
b 0
f 0
cc 1
eloc 36
nc 1
nop 30

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use function array_map;
18
use function array_unique;
19
use Assert\Assertion;
20
use Closure;
21
use DateTimeImmutable;
22
use const DIRECTORY_SEPARATOR;
23
use Herrera\Annotations\Tokenizer;
24
use Herrera\Box\Compactor\Php as LegacyPhp;
25
use Humbug\PhpScoper\Console\Configuration as PhpScoperConfiguration;
0 ignored issues
show
Bug introduced by
The type Humbug\PhpScoper\Console\Configuration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use InvalidArgumentException;
27
use function is_bool;
28
use KevinGH\Box\Compactor\Php;
29
use KevinGH\Box\Compactor\PhpScoper;
30
use Phar;
31
use RuntimeException;
32
use SplFileInfo;
33
use stdClass;
34
use Symfony\Component\Filesystem\Filesystem;
35
use Symfony\Component\Finder\Finder;
36
use Symfony\Component\Process\Process;
37
use function Humbug\PhpScoper\create_scoper;
0 ignored issues
show
introduced by
The function Humbug\PhpScoper\create_scoper was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
38
39
final class Configuration
40
{
41
    private const DEFAULT_ALIAS = 'default.phar';
42
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
43
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
44
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
45
46
    /**
47
     * @var null|Filesystem Available only when the Configuration is being instantiated
48
     */
49
    private static $fileSystem;
50
51
    private $fileMode;
52
    private $alias;
53
    private $basePathRetriever;
54
    private $files;
55
    private $binaryFiles;
56
    private $bootstrapFile;
57
    private $compactors;
58
    private $compressionAlgorithm;
59
    private $mainScriptPath;
60
    private $mainScriptContent;
61
    private $map;
62
    private $fileMapper;
63
    private $metadata;
64
    private $mimetypeMapping;
65
    private $mungVariables;
66
    private $notFoundScriptPath;
67
    private $outputPath;
68
    private $privateKeyPassphrase;
69
    private $privateKeyPath;
70
    private $isPrivateKeyPrompt;
71
    private $processedReplacements;
72
    private $shebang;
73
    private $signingAlgorithm;
74
    private $stubBanner;
75
    private $stubBannerPath;
76
    private $stubBannerFromFile;
77
    private $stubPath;
78
    private $isExtractable;
79
    private $isInterceptFileFuncs;
80
    private $isStubGenerated;
81
    private $isWebPhar;
82
83
    /**
84
     * @param string                   $alias
85
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
86
     * @param SplFileInfo[]            $files                 List of files
87
     * @param SplFileInfo[]            $binaryFiles           List of binary files
88
     * @param null|string              $bootstrapFile         The bootstrap file path
89
     * @param Compactor[]              $compactors            List of file contents compactors
90
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
91
     * @param null|int                 $fileMode              File mode in octal form
92
     * @param null|string              $mainScriptPath        The main script file path
93
     * @param null|string              $mainScriptContent     The processed content of the main script file
94
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
95
     * @param mixed                    $metadata              The PHAR Metadata
96
     * @param array                    $mimetypeMapping       The file extension MIME type mapping
97
     * @param array                    $mungVariables         The list of server variables to modify for execution
98
     * @param null|string              $notFoundScriptPath    The file path to the script to execute when a file is not found
99
     * @param string                   $outputPath
100
     * @param null|string              $privateKeyPassphrase
101
     * @param null|string              $privateKeyPath
102
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
103
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
104
     * @param null|string              $shebang               The shebang line
105
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
106
     * @param null|string              $stubBanner            The stub banner comment
107
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
108
     * @param null|string              $stubBannerFromFile    The stub banner comment from the fine
109
     * @param null|string              $stubPath              The PHAR stub file path
110
     * @param bool                     $isExtractable         Wether or not StubGenerator::extract() should be used
111
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
112
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
113
     * @param bool                     $isWebPhar             Wether or not the PHAR is going to be used for the web
114
     */
115
    private function __construct(
116
        string $alias,
117
        RetrieveRelativeBasePath $basePathRetriever,
118
        array $files,
119
        array $binaryFiles,
120
        ?string $bootstrapFile,
121
        array $compactors,
122
        ?int $compressionAlgorithm,
123
        ?int $fileMode,
124
        ?string $mainScriptPath,
125
        ?string $mainScriptContent,
126
        MapFile $fileMapper,
127
        $metadata,
128
        array $mimetypeMapping,
129
        array $mungVariables,
130
        ?string $notFoundScriptPath,
131
        string $outputPath,
132
        ?string $privateKeyPassphrase,
133
        ?string $privateKeyPath,
134
        bool $isPrivateKeyPrompt,
135
        array $processedReplacements,
136
        ?string $shebang,
137
        int $signingAlgorithm,
138
        ?string $stubBanner,
139
        ?string $stubBannerPath,
140
        ?string $stubBannerFromFile,
141
        ?string $stubPath,
142
        bool $isExtractable,
143
        bool $isInterceptFileFuncs,
144
        bool $isStubGenerated,
145
        bool $isWebPhar
146
    ) {
147
        Assertion::nullOrInArray(
148
            $compressionAlgorithm,
149
            get_phar_compression_algorithms(),
150
            sprintf(
151
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
152
                implode('", "', array_keys(get_phar_compression_algorithms()))
153
            )
154
        );
155
156
        $this->alias = $alias;
157
        $this->basePathRetriever = $basePathRetriever;
158
        $this->files = $files;
159
        $this->binaryFiles = $binaryFiles;
160
        $this->bootstrapFile = $bootstrapFile;
161
        $this->compactors = $compactors;
162
        $this->compressionAlgorithm = $compressionAlgorithm;
163
        $this->fileMode = $fileMode;
164
        $this->mainScriptPath = $mainScriptPath;
165
        $this->mainScriptContent = $mainScriptContent;
166
        $this->fileMapper = $fileMapper;
167
        $this->metadata = $metadata;
168
        $this->mimetypeMapping = $mimetypeMapping;
169
        $this->mungVariables = $mungVariables;
170
        $this->notFoundScriptPath = $notFoundScriptPath;
171
        $this->outputPath = $outputPath;
172
        $this->privateKeyPassphrase = $privateKeyPassphrase;
173
        $this->privateKeyPath = $privateKeyPath;
174
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
175
        $this->processedReplacements = $processedReplacements;
176
        $this->shebang = $shebang;
177
        $this->signingAlgorithm = $signingAlgorithm;
178
        $this->stubBanner = $stubBanner;
179
        $this->stubBannerPath = $stubBannerPath;
180
        $this->stubBannerFromFile = $stubBannerFromFile;
181
        $this->stubPath = $stubPath;
182
        $this->isExtractable = $isExtractable;
183
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
184
        $this->isStubGenerated = $isStubGenerated;
185
        $this->isWebPhar = $isWebPhar;
186
    }
187
188
    public static function create(string $file, stdClass $raw): self
189
    {
190
        self::$fileSystem = new Filesystem();
191
192
        $alias = self::retrieveAlias($raw);
193
194
        $basePath = self::retrieveBasePath($file, $raw);
195
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
196
197
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
198
199
        $files = self::retrieveFiles($raw, 'files', $basePath);
200
        $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
201
        $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
202
203
        $filesAggregate = array_unique(iterator_to_array(iterables_to_iterator($files, $directories, ...$filesFromFinders)));
204
205
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
206
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
207
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter);
208
209
        $binaryFilesAggregate = array_unique(iterator_to_array(iterables_to_iterator($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
210
211
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
212
213
        $compactors = self::retrieveCompactors($raw, $basePath);
214
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
215
216
        $fileMode = self::retrieveFileMode($raw);
217
218
        $mainScriptPath = self::retrieveMainScriptPath($raw);
219
        $mainScriptContent = self::retrieveMainScriptContents($mainScriptPath, $basePath);
220
221
        $map = self::retrieveMap($raw);
222
        $fileMapper = new MapFile($map);
223
224
        $metadata = self::retrieveMetadata($raw);
225
226
        $mimeTypeMapping = self::retrieveMimetypeMapping($raw);
227
        $mungVariables = self::retrieveMungVariables($raw);
228
        $notFoundScriptPath = self::retrieveNotFoundScriptPath($raw);
229
        $outputPath = self::retrieveOutputPath($raw, $file);
230
231
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
232
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
233
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
234
235
        $replacements = self::retrieveReplacements($raw);
236
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
237
238
        $shebang = self::retrieveShebang($raw);
239
240
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
241
242
        $stubBanner = self::retrieveStubBanner($raw);
243
        $stubBannerPath = self::retrieveStubBannerPath($raw);
244
        $stubBannerFromFile = self::retrieveStubBannerFromFile($basePath, $stubBannerPath);
245
246
        $stubPath = self::retrieveStubPath($raw);
247
248
        $isExtractable = self::retrieveIsExtractable($raw);
249
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
250
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
251
        $isWebPhar = self::retrieveIsWebPhar($raw);
252
253
        self::$fileSystem = null;
254
255
        return new self(
256
            $alias,
257
            $basePathRetriever,
258
            $filesAggregate,
259
            $binaryFilesAggregate,
260
            $bootstrapFile,
261
            $compactors,
262
            $compressionAlgorithm,
263
            $fileMode,
264
            $mainScriptPath,
265
            $mainScriptContent,
266
            $fileMapper,
267
            $metadata,
268
            $mimeTypeMapping,
269
            $mungVariables,
270
            $notFoundScriptPath,
271
            $outputPath,
272
            $privateKeyPassphrase,
273
            $privateKeyPath,
274
            $isPrivateKeyPrompt,
275
            $processedReplacements,
276
            $shebang,
277
            $signingAlgorithm,
278
            $stubBanner,
279
            $stubBannerPath,
280
            $stubBannerFromFile,
281
            $stubPath,
282
            $isExtractable,
283
            $isInterceptFileFuncs,
284
            $isStubGenerated,
285
            $isWebPhar
286
        );
287
    }
288
289
    public function getBasePathRetriever(): RetrieveRelativeBasePath
290
    {
291
        return $this->basePathRetriever;
292
    }
293
294
    public function getAlias(): string
295
    {
296
        return $this->alias;
297
    }
298
299
    public function getBasePath(): string
300
    {
301
        return $this->basePathRetriever->getBasePath();
302
    }
303
304
    /**
305
     * @return SplFileInfo[]
306
     */
307
    public function getFiles(): array
308
    {
309
        return $this->files;
310
    }
311
312
    /**
313
     * @return SplFileInfo[]
314
     */
315
    public function getBinaryFiles(): array
316
    {
317
        return $this->binaryFiles;
318
    }
319
320
    public function getBootstrapFile(): ?string
321
    {
322
        return $this->bootstrapFile;
323
    }
324
325
    public function loadBootstrap(): void
326
    {
327
        $file = $this->bootstrapFile;
328
329
        if (null !== $file) {
330
            include $file;
331
        }
332
    }
333
334
    /**
335
     * @return Compactor[] the list of compactors
336
     */
337
    public function getCompactors(): array
338
    {
339
        return $this->compactors;
340
    }
341
342
    public function getCompressionAlgorithm(): ?int
343
    {
344
        return $this->compressionAlgorithm;
345
    }
346
347
    public function getFileMode(): ?int
348
    {
349
        return $this->fileMode;
350
    }
351
352
    public function getMainScriptPath(): ?string
353
    {
354
        return $this->mainScriptPath;
355
    }
356
357
    public function getMainScriptContent(): ?string
358
    {
359
        return $this->mainScriptContent;
360
    }
361
362
    public function getMimetypeMapping(): array
363
    {
364
        return $this->mimetypeMapping;
365
    }
366
367
    public function getMungVariables(): array
368
    {
369
        return $this->mungVariables;
370
    }
371
372
    public function getNotFoundScriptPath(): ?string
373
    {
374
        return $this->notFoundScriptPath;
375
    }
376
377
    public function getOutputPath(): string
378
    {
379
        return $this->outputPath;
380
    }
381
382
    /**
383
     * @return string[]
384
     */
385
    public function getMap(): array
386
    {
387
        return $this->fileMapper->getMap();
388
    }
389
390
    public function getFileMapper(): MapFile
391
    {
392
        return $this->fileMapper;
393
    }
394
395
    /**
396
     * @return mixed
397
     */
398
    public function getMetadata()
399
    {
400
        return $this->metadata;
401
    }
402
403
    public function getPrivateKeyPassphrase(): ?string
404
    {
405
        return $this->privateKeyPassphrase;
406
    }
407
408
    public function getPrivateKeyPath(): ?string
409
    {
410
        return $this->privateKeyPath;
411
    }
412
413
    public function isPrivateKeyPrompt(): bool
414
    {
415
        return $this->isPrivateKeyPrompt;
416
    }
417
418
    public function getProcessedReplacements(): array
419
    {
420
        return $this->processedReplacements;
421
    }
422
423
    public function getShebang(): ?string
424
    {
425
        return $this->shebang;
426
    }
427
428
    public function getSigningAlgorithm(): int
429
    {
430
        return $this->signingAlgorithm;
431
    }
432
433
    public function getStubBanner(): ?string
434
    {
435
        return $this->stubBanner;
436
    }
437
438
    public function getStubBannerPath(): ?string
439
    {
440
        return $this->stubBannerPath;
441
    }
442
443
    public function getStubBannerFromFile()
444
    {
445
        return $this->stubBannerFromFile;
446
    }
447
448
    public function getStubPath(): ?string
449
    {
450
        return $this->stubPath;
451
    }
452
453
    public function isExtractable(): bool
454
    {
455
        return $this->isExtractable;
456
    }
457
458
    public function isInterceptFileFuncs(): bool
459
    {
460
        return $this->isInterceptFileFuncs;
461
    }
462
463
    public function isStubGenerated(): bool
464
    {
465
        return $this->isStubGenerated;
466
    }
467
468
    public function isWebPhar(): bool
469
    {
470
        return $this->isWebPhar;
471
    }
472
473
    private static function retrieveAlias(stdClass $raw): string
474
    {
475
        $alias = $raw->alias ?? self::DEFAULT_ALIAS;
476
477
        $alias = trim($alias);
478
479
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty.');
480
481
        return $alias;
482
    }
483
484
    private static function retrieveBasePath(string $file, stdClass $raw): string
485
    {
486
        if (false === isset($raw->{'base-path'})) {
487
            return realpath(dirname($file));
488
        }
489
490
        $basePath = trim($raw->{'base-path'});
491
492
        Assertion::directory(
493
            $basePath,
494
            'The base path "%s" is not a directory or does not exist.'
495
        );
496
497
        return realpath($basePath);
498
    }
499
500
    /**
501
     * @param stdClass $raw
502
     * @param string   $basePath
503
     *
504
     * @return Closure
505
     */
506
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
507
    {
508
        $blacklist = self::retrieveBlacklist($raw, $basePath);
509
510
        return function (SplFileInfo $file) use ($blacklist): ?bool {
511
            if (in_array($file->getRealPath(), $blacklist, true)) {
512
                return false;
513
            }
514
515
            return null;
516
        };
517
    }
518
519
    /**
520
     * @return string[]
521
     */
522
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
523
    {
524
        if (false === isset($raw->blacklist)) {
525
            return [];
526
        }
527
528
        $blacklist = $raw->blacklist;
529
530
        $normalizePath = function ($file) use ($basePath): string {
531
            return self::normalizeFilePath($file, $basePath);
532
        };
533
534
        return array_map($normalizePath, $blacklist);
535
    }
536
537
    /**
538
     * @param stdClass $raw
539
     * @param string   $key      Config property name
540
     * @param string   $basePath
541
     *
542
     * @return SplFileInfo[]
543
     */
544
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
545
    {
546
        if (false === isset($raw->{$key})) {
547
            return [];
548
        }
549
550
        $files = (array) $raw->{$key};
551
552
        Assertion::allString($files);
553
554
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
555
            $file = self::normalizeFilePath($file, $basePath);
556
557
            Assertion::file(
558
                $file,
559
                sprintf(
560
                    '"%s" must contain a list of existing files. Could not find "%%s".',
561
                    $key
562
                )
563
            );
564
565
            return new SplFileInfo($file);
566
        };
567
568
        return array_map($normalizePath, $files);
569
    }
570
571
    /**
572
     * @param stdClass $raw
573
     * @param string   $key             Config property name
574
     * @param string   $basePath
575
     * @param Closure  $blacklistFilter
576
     *
577
     * @return iterable|SplFileInfo[]
578
     */
579
    private static function retrieveDirectories(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): iterable
580
    {
581
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
582
583
        if ([] !== $directories) {
584
            return Finder::create()
1 ignored issue
show
Bug Best Practice introduced by
The expression return Symfony\Component...true)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
585
                ->files()
586
                ->filter($blacklistFilter)
587
                ->ignoreVCS(true)
588
                ->in($directories)
589
            ;
590
        }
591
592
        return [];
593
    }
594
595
    /**
596
     * @param stdClass $raw
597
     * @param string   $basePath
598
     * @param Closure  $blacklistFilter
599
     *
600
     * @return iterable[]|SplFileInfo[][]
601
     */
602
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
603
    {
604
        if (isset($raw->{$key})) {
605
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
606
        }
607
608
        return [];
609
    }
610
611
    /**
612
     * @param array   $findersConfig   the configuration
613
     * @param string  $basePath
614
     * @param Closure $blacklistFilter
615
     *
616
     * @return Finder[]|SplFileInfo[][]
617
     */
618
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
619
    {
620
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter) {
621
            return self::processFinder($config, $basePath, $blacklistFilter);
622
        };
623
624
        return array_map($processFinderConfig, $findersConfig);
625
    }
626
627
    /**
628
     * @param array   $findersConfig   the configuration
629
     * @param string  $basePath
630
     * @param Closure $blacklistFilter
631
     *
632
     * @return Finder
633
     */
634
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter): Finder
635
    {
636
        $finder = Finder::create()
637
            ->files()
638
            ->filter($blacklistFilter)
639
            ->ignoreVCS(true)
640
        ;
641
642
        $normalizedConfig = (function (array $config, Finder $finder): array {
643
            $normalizedConfig = [];
644
645
            foreach ($config as $method => $arguments) {
646
                $method = trim($method);
647
                $arguments = (array) $arguments;
648
649
                Assertion::methodExists(
650
                    $method,
651
                    $finder,
652
                    'The method "Finder::%s" does not exist.'
653
                );
654
655
                $normalizedConfig[$method] = $arguments;
656
            }
657
658
            krsort($normalizedConfig);
659
660
            return $normalizedConfig;
661
        })((array) $config, $finder);
662
663
        $createNormalizedDirectories = function (string $directory) use ($basePath): string {
664
            $directory = self::normalizeDirectoryPath($directory, $basePath);
665
666
            Assertion::directory($directory);
667
668
            return $directory;
669
        };
670
671
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
672
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
673
674
            if (false === file_exists($fileOrDirectory)) {
675
                throw new InvalidArgumentException(
676
                    sprintf(
677
                        'Path "%s" was expected to be a file or directory.',
678
                        $fileOrDirectory
679
                    )
680
                );
681
            }
682
683
            if (false === is_file($fileOrDirectory)) {
684
                Assertion::directory($fileOrDirectory);
685
            } else {
686
                Assertion::file($fileOrDirectory);
687
            }
688
        };
689
690
        foreach ($normalizedConfig as $method => $arguments) {
691
            if ('in' === $method) {
692
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
693
            }
694
695
            if ('exclude' === $method) {
696
                $arguments = array_unique(array_map('trim', $arguments));
697
            }
698
699
            if ('append' === $method) {
700
                array_walk($arguments, $normalizeFileOrDirectory);
701
702
                $arguments = [$arguments];
703
            }
704
705
            foreach ($arguments as $argument) {
706
                $finder->$method($argument);
707
            }
708
        }
709
710
        return $finder;
711
    }
712
713
    /**
714
     * @param stdClass $raw
715
     * @param string   $key      Config property name
716
     * @param string   $basePath
717
     *
718
     * @return string[]
719
     */
720
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
721
    {
722
        if (false === isset($raw->{$key})) {
723
            return [];
724
        }
725
726
        $directories = $raw->{$key};
727
728
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
729
            $directory = self::normalizeDirectoryPath($directory, $basePath);
730
731
            Assertion::directory(
732
                $directory,
733
                sprintf(
734
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
735
                    $key
736
                )
737
            );
738
739
            return $directory;
740
        };
741
742
        return array_map($normalizeDirectory, $directories);
743
    }
744
745
    private static function normalizeFilePath(string $file, string $basePath): string
746
    {
747
        $file = trim($file);
748
749
        if (false === self::$fileSystem->isAbsolutePath($file)) {
0 ignored issues
show
Bug introduced by
The method isAbsolutePath() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

749
        if (false === self::$fileSystem->/** @scrutinizer ignore-call */ isAbsolutePath($file)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
750
            $file = $basePath.DIRECTORY_SEPARATOR.canonicalize($file);
751
        }
752
753
        return $file;
754
    }
755
756
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
757
    {
758
        $directory = trim($directory);
759
760
        if (false === self::$fileSystem->isAbsolutePath($directory)) {
761
            $directory = sprintf(
762
                '%s%s',
763
                $basePath.DIRECTORY_SEPARATOR,
764
                rtrim(
765
                    canonicalize($directory),
766
                    DIRECTORY_SEPARATOR
767
                )
768
            );
769
        }
770
771
        return $directory;
772
    }
773
774
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
775
    {
776
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
777
        // through that extension point so this is pretty much useless unless proven otherwise.
778
        if (false === isset($raw->bootstrap)) {
779
            return null;
780
        }
781
782
        $file = $raw->bootstrap;
783
784
        if (false === is_absolute($file)) {
785
            $file = canonicalize(
786
                $basePath.DIRECTORY_SEPARATOR.$file
787
            );
788
        }
789
790
        if (false === file_exists($file)) {
791
            throw new InvalidArgumentException(
792
                sprintf(
793
                    'The bootstrap path "%s" is not a file or does not exist.',
794
                    $file
795
                )
796
            );
797
        }
798
799
        return $file;
800
    }
801
802
    /**
803
     * @return Compactor[]
804
     */
805
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
806
    {
807
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
808
        if (false === isset($raw->compactors)) {
809
            return [];
810
        }
811
812
        $compactorClasses = array_unique((array) $raw->compactors);
813
814
        return array_map(
815
            function (string $class) use ($raw, $basePath): Compactor {
816
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
817
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
818
819
                if (Php::class === $class || LegacyPhp::class === $class) {
820
                    return self::createPhpCompactor($raw);
821
                }
822
823
                if (PhpScoper::class === $class) {
824
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
825
826
                    return new PhpScoper(create_scoper(), $phpScoperConfig);
0 ignored issues
show
Bug introduced by
The function create_scoper was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

826
                    return new PhpScoper(/** @scrutinizer ignore-call */ create_scoper(), $phpScoperConfig);
Loading history...
827
                }
828
829
                return new $class();
830
            },
831
            $compactorClasses
832
        );
833
    }
834
835
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
836
    {
837
        // TODO: if in dev mode (when added), do not comment about the compression.
838
        // If not, add a warning to notify the user if no compression algorithm is used
839
        // provided the PHAR is not configured for web purposes.
840
        // If configured for the web, add a warning when a compression algorithm is used
841
        // as this can result in an overhead. Add a doc link explaining this.
842
        //
843
        // Unlike the doc: do not accept integers and document this BC break.
844
        if (false === isset($raw->compression)) {
845
            return null;
846
        }
847
848
        if (false === is_string($raw->compression)) {
849
            Assertion::integer(
850
                $raw->compression,
851
                'Expected compression to be an algorithm name, found %s instead.'
852
            );
853
854
            return $raw->compression;
1 ignored issue
show
Bug Best Practice introduced by
The expression return $raw->compression returns the type string which is incompatible with the type-hinted return null|integer.
Loading history...
855
        }
856
857
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
858
859
        Assertion::inArray(
860
            $raw->compression,
861
            $knownAlgorithmNames,
862
            sprintf(
863
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
864
                implode('", "', $knownAlgorithmNames)
865
            )
866
        );
867
868
        $value = get_phar_compression_algorithms()[$raw->compression];
869
870
        // Phar::NONE is not valid for compressFiles()
871
        if (Phar::NONE === $value) {
872
            return null;
873
        }
874
875
        return $value;
876
    }
877
878
    private static function retrieveFileMode(stdClass $raw): ?int
879
    {
880
        if (isset($raw->chmod)) {
881
            return intval($raw->chmod, 8);
882
        }
883
884
        return null;
885
    }
886
887
    private static function retrieveMainScriptPath(stdClass $raw): ?string
888
    {
889
        // TODO: check if is used for the web as well when web is set to true
890
        // If that the case make this field mandatory otherwise adjust the check
891
        // rules accordinly to ensure we do not have an empty PHAR
892
        if (isset($raw->main)) {
893
            return canonicalize($raw->main);
894
        }
895
896
        return null;
897
    }
898
899
    private static function retrieveMainScriptContents(?string $mainScriptPath, string $basePath): ?string
900
    {
901
        if (null === $mainScriptPath) {
902
            return null;
903
        }
904
905
        $mainScriptPath = $basePath.DIRECTORY_SEPARATOR.$mainScriptPath;
906
907
        Assertion::readable($mainScriptPath);
908
909
        $contents = file_get_contents($mainScriptPath);
910
911
        // Remove the shebang line
912
        return preg_replace('/^#!.*\s*/', '', $contents);
913
    }
914
915
    /**
916
     * @return string[][]
917
     */
918
    private static function retrieveMap(stdClass $raw): array
919
    {
920
        if (false === isset($raw->map)) {
921
            return [];
922
        }
923
924
        $map = [];
925
926
        foreach ((array) $raw->map as $item) {
927
            $processed = [];
928
929
            foreach ($item as $match => $replace) {
930
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
931
            }
932
933
            if (isset($processed['_empty_'])) {
934
                $processed[''] = $processed['_empty_'];
935
936
                unset($processed['_empty_']);
937
            }
938
939
            $map[] = $processed;
940
        }
941
942
        return $map;
943
    }
944
945
    /**
946
     * @return mixed
947
     */
948
    private static function retrieveMetadata(stdClass $raw)
949
    {
950
        // TODO: the doc currently say this can be any value; check if true
951
        // and if not add checks accordingly
952
        //
953
        // Also review the doc as I don't find it very helpful...
954
        if (isset($raw->metadata)) {
955
            if (is_object($raw->metadata)) {
956
                return (array) $raw->metadata;
957
            }
958
959
            return $raw->metadata;
960
        }
961
962
        return null;
963
    }
964
965
    private static function retrieveMimetypeMapping(stdClass $raw): array
966
    {
967
        // TODO: this parameter is not clear to me: review usage, doc & checks
968
        if (isset($raw->mimetypes)) {
969
            return (array) $raw->mimetypes;
970
        }
971
972
        return [];
973
    }
974
975
    private static function retrieveMungVariables(stdClass $raw): array
976
    {
977
        // TODO: this parameter is not clear to me: review usage, doc & checks
978
        // TODO: add error/warning if used when web is not enabled
979
        if (isset($raw->mung)) {
980
            return (array) $raw->mung;
981
        }
982
983
        return [];
984
    }
985
986
    private static function retrieveNotFoundScriptPath(stdClass $raw): ?string
987
    {
988
        // TODO: this parameter is not clear to me: review usage, doc & checks
989
        // TODO: add error/warning if used when web is not enabled
990
        if (isset($raw->{'not-found'})) {
991
            return $raw->{'not-found'};
992
        }
993
994
        return null;
995
    }
996
997
    private static function retrieveOutputPath(stdClass $raw, string $file): string
998
    {
999
        // TODO: make this path relative to the base path like everything else
1000
        // otherwise this is really confusing. This is a BC break that needs to be
1001
        // documented though (and update the doc accordingly as well)
1002
        $base = getcwd().DIRECTORY_SEPARATOR;
1003
1004
        if (isset($raw->output)) {
1005
            $path = $raw->output;
1006
1007
            if (false === is_absolute($path)) {
1008
                $path = canonicalize($base.$path);
1009
            }
1010
        } else {
1011
            $path = $base.self::DEFAULT_ALIAS;
1012
        }
1013
1014
        if (false !== strpos($path, '@'.'git-version@')) {
1015
            $gitVersion = self::retrieveGitVersion($file);
1016
1017
            $path = str_replace('@'.'git-version@', $gitVersion, $path);
1018
        }
1019
1020
        return $path;
1021
    }
1022
1023
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1024
    {
1025
        // TODO: add check to not allow this setting without the private key path
1026
        if (isset($raw->{'key-pass'})
1027
            && is_string($raw->{'key-pass'})
1028
        ) {
1029
            return $raw->{'key-pass'};
1030
        }
1031
1032
        return null;
1033
    }
1034
1035
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1036
    {
1037
        // TODO: If passed need to check its existence
1038
        // Also need
1039
1040
        if (isset($raw->key)) {
1041
            return $raw->key;
1042
        }
1043
1044
        return null;
1045
    }
1046
1047
    private static function retrieveReplacements(stdClass $raw): array
1048
    {
1049
        // TODO: add exmample in the doc
1050
        // Add checks against the values
1051
        if (isset($raw->replacements)) {
1052
            return (array) $raw->replacements;
1053
        }
1054
1055
        return [];
1056
    }
1057
1058
    private static function retrieveProcessedReplacements(
1059
        array $replacements,
1060
        stdClass $raw,
1061
        string $file
1062
    ): array {
1063
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1064
            $replacements[$git] = self::retrieveGitHash($file);
1065
        }
1066
1067
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1068
            $replacements[$git] = self::retrieveGitHash($file, true);
1069
        }
1070
1071
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1072
            $replacements[$git] = self::retrieveGitTag($file);
1073
        }
1074
1075
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1076
            $replacements[$git] = self::retrieveGitVersion($file);
1077
        }
1078
1079
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1080
            $replacements[$date] = self::retrieveDatetimeNow(
1081
                self::retrieveDatetimeFormat($raw)
1082
            );
1083
        }
1084
1085
        $sigil = self::retrieveReplacementSigil($raw);
1086
1087
        foreach ($replacements as $key => $value) {
1088
            unset($replacements[$key]);
1089
            $replacements["$sigil$key$sigil"] = $value;
1090
        }
1091
1092
        return $replacements;
1093
    }
1094
1095
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1096
    {
1097
        if (isset($raw->{'git-commit'})) {
1098
            return $raw->{'git-commit'};
1099
        }
1100
1101
        return null;
1102
    }
1103
1104
    /**
1105
     * @param string $file
1106
     * @param bool   $short Use the short version
1107
     *
1108
     * @return string the commit hash
1109
     */
1110
    private static function retrieveGitHash(string $file, bool $short = false): string
1111
    {
1112
        return self::runGitCommand(
1113
            sprintf(
1114
                'git log --pretty="%s" -n1 HEAD',
1115
                $short ? '%h' : '%H'
1116
            ),
1117
            $file
1118
        );
1119
    }
1120
1121
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1122
    {
1123
        if (isset($raw->{'git-commit-short'})) {
1124
            return $raw->{'git-commit-short'};
1125
        }
1126
1127
        return null;
1128
    }
1129
1130
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1131
    {
1132
        if (isset($raw->{'git-tag'})) {
1133
            return $raw->{'git-tag'};
1134
        }
1135
1136
        return null;
1137
    }
1138
1139
    private static function retrieveGitTag(string $file): ?string
1140
    {
1141
        return self::runGitCommand('git describe --tags HEAD', $file);
1142
    }
1143
1144
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1145
    {
1146
        if (isset($raw->{'git-version'})) {
1147
            return $raw->{'git-version'};
1148
        }
1149
1150
        return null;
1151
    }
1152
1153
    private static function retrieveGitVersion(string $file): ?string
1154
    {
1155
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1156
        // to avoid messing around with that
1157
1158
        try {
1159
            return self::retrieveGitTag($file);
1160
        } catch (RuntimeException $exception) {
1161
            try {
1162
                return self::retrieveGitHash($file, true);
1163
            } catch (RuntimeException $exception) {
1164
                throw new RuntimeException(
1165
                    sprintf(
1166
                        'The tag or commit hash could not be retrieved from "%s": %s',
1167
                        dirname($file),
1168
                        $exception->getMessage()
1169
                    ),
1170
                    0,
1171
                    $exception
1172
                );
1173
            }
1174
        }
1175
    }
1176
1177
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1178
    {
1179
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1180
        // Also make sure the documentation is up to date after.
1181
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1182
        // be better to have only one element IMO like:
1183
        //
1184
        // "datetime": {
1185
        //   "value": "val",
1186
        //   "format": "Y-m-d"
1187
        // }
1188
        //
1189
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1190
        // if the datetime format is the default one it's ok; but in any case the format should not
1191
        // be added without the datetime value...
1192
1193
        if (isset($raw->{'datetime'})) {
1194
            return $raw->{'datetime'};
1195
        }
1196
1197
        return null;
1198
    }
1199
1200
    private static function retrieveDatetimeNow(string $format)
1201
    {
1202
        $now = new DateTimeImmutable('now');
1203
1204
        $datetime = $now->format($format);
1205
1206
        if (!$datetime) {
1207
            throw new InvalidArgumentException(
1208
                sprintf(
1209
                    '""%s" is not a valid PHP date format',
1210
                    $format
1211
                )
1212
            );
1213
        }
1214
1215
        return $datetime;
1216
    }
1217
1218
    private static function retrieveDatetimeFormat(stdClass $raw): string
1219
    {
1220
        if (isset($raw->{'datetime_format'})) {
1221
            return $raw->{'datetime_format'};
1222
        }
1223
1224
        return self::DEFAULT_DATETIME_FORMAT;
1225
    }
1226
1227
    private static function retrieveReplacementSigil(stdClass $raw)
1228
    {
1229
        if (isset($raw->{'replacement-sigil'})) {
1230
            return $raw->{'replacement-sigil'};
1231
        }
1232
1233
        return self::DEFAULT_REPLACEMENT_SIGIL;
1234
    }
1235
1236
    private static function retrieveShebang(stdClass $raw): ?string
1237
    {
1238
        // TODO: unlike the doc says do not allow empty strings.
1239
        // Leverage `Assertion` here?
1240
        if (false === isset($raw->shebang)) {
1241
            return null;
1242
        }
1243
1244
        if (('' === $raw->shebang) || (false === $raw->shebang)) {
1245
            return '';
1246
        }
1247
1248
        $shebang = trim($raw->shebang);
1249
1250
        if ('#!' !== substr($shebang, 0, 2)) {
1251
            throw new InvalidArgumentException(
1252
                sprintf(
1253
                    'The shebang line must start with "#!": %s',
1254
                    $shebang
1255
                )
1256
            );
1257
        }
1258
1259
        return $shebang;
1260
    }
1261
1262
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1263
    {
1264
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1265
        // TODO: trigger a warning if the signing algorithm used is weak
1266
        // TODO: no longer accept strings & document BC break
1267
        if (false === isset($raw->algorithm)) {
1268
            return Phar::SHA1;
1269
        }
1270
1271
        if (is_string($raw->algorithm)) {
1272
            if (false === defined('Phar::'.$raw->algorithm)) {
1273
                throw new InvalidArgumentException(
1274
                    sprintf(
1275
                        'The signing algorithm "%s" is not supported.',
1276
                        $raw->algorithm
1277
                    )
1278
                );
1279
            }
1280
1281
            return constant('Phar::'.$raw->algorithm);
1282
        }
1283
1284
        return $raw->algorithm;
1285
    }
1286
1287
    private static function retrieveStubBanner(stdClass $raw): ?string
1288
    {
1289
        if (isset($raw->{'banner'})) {
1290
            return $raw->{'banner'};
1291
        }
1292
1293
        return null;
1294
    }
1295
1296
    private static function retrieveStubBannerPath(stdClass $raw): ?string
1297
    {
1298
        // TODO: if provided check its existence here or should it be defered to later?
1299
        // Works case this check can be duplicated...
1300
        //
1301
        // Check if is relative to base path: if not make it so (may be a BC break to document).
1302
        // Once checked, a mention in the doc that this path is relative to base-path (unless
1303
        // absolute).
1304
        // Check that the path is not provided if a banner is already provided.
1305
        if (isset($raw->{'banner-file'})) {
1306
            return canonicalize($raw->{'banner-file'});
1307
        }
1308
1309
        return null;
1310
    }
1311
1312
    private static function retrieveStubBannerFromFile(string $basePath, ?string $stubBannerPath): ?string
1313
    {
1314
        // TODO: Add checks
1315
        // TODO: The documentation is not clear enough IMO
1316
        if (null == $stubBannerPath) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $stubBannerPath of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1317
            return null;
1318
        }
1319
1320
        $stubBannerPath = $basePath.DIRECTORY_SEPARATOR.$stubBannerPath;
1321
1322
        if (false === ($contents = @file_get_contents($stubBannerPath))) {
0 ignored issues
show
introduced by
The condition false === $contents = @f...ntents($stubBannerPath) can never be true.
Loading history...
1323
            $errors = error_get_last();
1324
1325
            if (null === $errors) {
1326
                $errors = ['message' => 'failed to get contents of "'.$stubBannerPath.'""'];
1327
            }
1328
1329
            throw new InvalidArgumentException($errors['message']);
1330
        }
1331
1332
        return $contents;
1333
    }
1334
1335
    private static function retrieveStubPath(stdClass $raw): ?string
1336
    {
1337
        if (isset($raw->stub) && is_string($raw->stub)) {
1338
            return $raw->stub;
1339
        }
1340
1341
        return null;
1342
    }
1343
1344
    private static function retrieveIsExtractable(stdClass $raw): bool
1345
    {
1346
        // TODO: look it up, really not clear to me neither is the doc
1347
        if (isset($raw->extract)) {
1348
            return $raw->extract;
1349
        }
1350
1351
        return false;
1352
    }
1353
1354
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1355
    {
1356
        if (isset($raw->intercept)) {
1357
            return $raw->intercept;
1358
        }
1359
1360
        return false;
1361
    }
1362
1363
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1364
    {
1365
        if (isset($raw->{'key-pass'})
1366
            && (true === $raw->{'key-pass'})) {
1367
            return true;
1368
        }
1369
1370
        return false;
1371
    }
1372
1373
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1374
    {
1375
        if (isset($raw->stub) && (true === $raw->stub)) {
1376
            return true;
1377
        }
1378
1379
        return false;
1380
    }
1381
1382
    private static function retrieveIsWebPhar(stdClass $raw): bool
1383
    {
1384
        // TODO: doc is not clear enough
1385
        // Also check if is compatible web + CLI
1386
        if (isset($raw->web)) {
1387
            return $raw->web;
1388
        }
1389
1390
        return false;
1391
    }
1392
1393
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1394
    {
1395
        if (!isset($raw->{'php-scoper'})) {
1396
            return PhpScoperConfiguration::load();
1397
        }
1398
1399
        $configFile = $raw->phpScoper;
1400
1401
        Assertion::string($configFile);
1402
1403
        if (false === self::$fileSystem->isAbsolutePath($configFile)) {
1404
            $configFilePath = $basePath.DIRECTORY_SEPARATOR.$configFile;
1405
        } else {
1406
            $configFilePath = $configFile;
1407
        }
1408
1409
        Assertion::file($configFilePath);
1410
        Assertion::readable($configFilePath);
1411
1412
        return PhpScoperConfiguration::load($configFilePath);
1413
    }
1414
1415
    /**
1416
     * Runs a Git command on the repository.
1417
     *
1418
     * @param string $command the command
1419
     *
1420
     * @return string the trimmed output from the command
1421
     */
1422
    private static function runGitCommand(string $command, string $file): string
1423
    {
1424
        $path = dirname($file);
1425
1426
        $process = new Process($command, $path);
1427
1428
        if (0 === $process->run()) {
1429
            return trim($process->getOutput());
1430
        }
1431
1432
        throw new RuntimeException(
1433
            sprintf(
1434
                'The tag or commit hash could not be retrieved from "%s": %s',
1435
                $path,
1436
                $process->getErrorOutput()
1437
            )
1438
        );
1439
    }
1440
1441
    private static function createPhpCompactor(stdClass $raw): Compactor
1442
    {
1443
        // TODO: false === not set; check & add test/doc
1444
        $tokenizer = new Tokenizer();
1445
1446
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1447
            $tokenizer->ignore(
1448
                (array) $raw->annotations->ignore
1449
            );
1450
        }
1451
1452
        return new Php($tokenizer);
1453
    }
1454
}
1455