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

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