Completed
Pull Request — master (#42)
by Théo
02:05
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 InvalidArgumentException;
23
use KevinGH\Box\Compactor\Php;
24
use Phar;
25
use RuntimeException;
26
use SplFileInfo;
27
use stdClass;
28
use Symfony\Component\Filesystem\Filesystem;
29
use Symfony\Component\Finder\Finder;
30
use Symfony\Component\Process\Process;
31
32
final class Configuration
33
{
34
    private const DEFAULT_ALIAS = 'default.phar';
35
    private const DEFAULT_MAIN = 'index.php';   // See Phar::setDefaultStub()
36
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
37
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
38
39
    /**
40
     * @var null|Filesystem Available only when the Configuration is being instantiated
41
     */
42
    private static $fileSystem;
43
44
    private $fileMode;
45
    private $alias;
46
    private $basePathRetriever;
47
    private $files;
48
    private $binaryFiles;
49
    private $bootstrapFile;
50
    private $compactors;
51
    private $compressionAlgorithm;
52
    private $mainScriptPath;
53
    private $mainScriptContent;
54
    private $map;
55
    private $fileMapper;
56
    private $metadata;
57
    private $mimetypeMapping;
58
    private $mungVariables;
59
    private $notFoundScriptPath;
60
    private $outputPath;
61
    private $privateKeyPassphrase;
62
    private $privateKeyPath;
63
    private $isPrivateKeyPrompt;
64
    private $processedReplacements;
65
    private $shebang;
66
    private $signingAlgorithm;
67
    private $stubBanner;
68
    private $stubBannerPath;
69
    private $stubBannerFromFile;
70
    private $stubPath;
71
    private $isExtractable;
72
    private $isInterceptFileFuncs;
73
    private $isStubGenerated;
74
    private $isWebPhar;
75
76
    /**
77
     * @param string                   $alias
78
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
79
     * @param SplFileInfo[]            $files                 List of files
80
     * @param SplFileInfo[]            $binaryFiles           List of binary files
81
     * @param null|string              $bootstrapFile         The bootstrap file path
82
     * @param Compactor[]              $compactors            List of file contents compactors
83
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
84
     * @param null|int                 $fileMode              File mode in octal form
85
     * @param null|string              $mainScriptPath        The main script file path
86
     * @param null|string              $mainScriptContent     The processed content of the main script file
87
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
88
     * @param mixed                    $metadata              The PHAR Metadata
89
     * @param array                    $mimetypeMapping       The file extension MIME type mapping
90
     * @param array                    $mungVariables         The list of server variables to modify for execution
91
     * @param null|string              $notFoundScriptPath    The file path to the script to execute when a file is not found
92
     * @param string                   $outputPath
93
     * @param null|string              $privateKeyPassphrase
94
     * @param null|string              $privateKeyPath
95
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
96
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
97
     * @param null|string              $shebang               The shebang line
98
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
99
     * @param null|string              $stubBanner            The stub banner comment
100
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
101
     * @param null|string              $stubBannerFromFile    The stub banner comment from the fine
102
     * @param null|string              $stubPath              The PHAR stub file path
103
     * @param bool                     $isExtractable         Wether or not StubGenerator::extract() should be used
104
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
105
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
106
     * @param bool                     $isWebPhar             Wether or not the PHAR is going to be used for the web
107
     */
108
    private function __construct(
109
        string $alias,
110
        RetrieveRelativeBasePath $basePathRetriever,
111
        array $files,
112
        array $binaryFiles,
113
        ?string $bootstrapFile,
114
        array $compactors,
115
        ?int $compressionAlgorithm,
116
        ?int $fileMode,
117
        ?string $mainScriptPath,
118
        ?string $mainScriptContent,
119
        MapFile $fileMapper,
120
        $metadata,
121
        array $mimetypeMapping,
122
        array $mungVariables,
123
        ?string $notFoundScriptPath,
124
        string $outputPath,
125
        ?string $privateKeyPassphrase,
126
        ?string $privateKeyPath,
127
        bool $isPrivateKeyPrompt,
128
        array $processedReplacements,
129
        ?string $shebang,
130
        int $signingAlgorithm,
131
        ?string $stubBanner,
132
        ?string $stubBannerPath,
133
        ?string $stubBannerFromFile,
134
        ?string $stubPath,
135
        bool $isExtractable,
136
        bool $isInterceptFileFuncs,
137
        bool $isStubGenerated,
138
        bool $isWebPhar
139
    ) {
140
        Assertion::nullOrInArray(
141
            $compressionAlgorithm,
142
            get_phar_compression_algorithms(),
143
            sprintf(
144
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
145
                implode('", "', array_keys(get_phar_compression_algorithms()))
146
            )
147
        );
148
149
        $this->alias = $alias;
150
        $this->basePathRetriever = $basePathRetriever;
151
        $this->files = $files;
152
        $this->binaryFiles = $binaryFiles;
153
        $this->bootstrapFile = $bootstrapFile;
154
        $this->compactors = $compactors;
155
        $this->compressionAlgorithm = $compressionAlgorithm;
156
        $this->fileMode = $fileMode;
157
        $this->mainScriptPath = $mainScriptPath;
158
        $this->mainScriptContent = $mainScriptContent;
159
        $this->fileMapper = $fileMapper;
160
        $this->metadata = $metadata;
161
        $this->mimetypeMapping = $mimetypeMapping;
162
        $this->mungVariables = $mungVariables;
163
        $this->notFoundScriptPath = $notFoundScriptPath;
164
        $this->outputPath = $outputPath;
165
        $this->privateKeyPassphrase = $privateKeyPassphrase;
166
        $this->privateKeyPath = $privateKeyPath;
167
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
168
        $this->processedReplacements = $processedReplacements;
169
        $this->shebang = $shebang;
170
        $this->signingAlgorithm = $signingAlgorithm;
171
        $this->stubBanner = $stubBanner;
172
        $this->stubBannerPath = $stubBannerPath;
173
        $this->stubBannerFromFile = $stubBannerFromFile;
174
        $this->stubPath = $stubPath;
175
        $this->isExtractable = $isExtractable;
176
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
177
        $this->isStubGenerated = $isStubGenerated;
178
        $this->isWebPhar = $isWebPhar;
179
    }
180
181
    public static function create(string $file, stdClass $raw): self
182
    {
183
        self::$fileSystem = new Filesystem();
184
185
        $alias = self::retrieveAlias($raw);
186
187
        $basePath = self::retrieveBasePath($file, $raw);
188
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
189
190
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
191
192
        $files = self::retrieveFiles($raw, 'files', $basePath);
193
        $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
194
        $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
195
196
        $filesAggregate = array_unique(iterator_to_array(iterables_to_iterator($files, $directories, ...$filesFromFinders)));
197
198
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
199
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
200
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter);
201
202
        $binaryFilesAggregate = array_unique(iterator_to_array(iterables_to_iterator($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
203
204
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
205
206
        $compactors = self::retrieveCompactors($raw);
207
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
208
209
        $fileMode = self::retrieveFileMode($raw);
210
211
        $mainScriptPath = self::retrieveMainScriptPath($raw);
212
        $mainScriptContent = self::retrieveMainScriptContents($mainScriptPath, $basePath);
213
214
        $map = self::retrieveMap($raw);
215
        $fileMapper = new MapFile($map);
216
217
        $metadata = self::retrieveMetadata($raw);
218
219
        $mimeTypeMapping = self::retrieveMimetypeMapping($raw);
220
        $mungVariables = self::retrieveMungVariables($raw);
221
        $notFoundScriptPath = self::retrieveNotFoundScriptPath($raw);
222
        $outputPath = self::retrieveOutputPath($raw, $file);
223
224
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
225
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
226
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
227
228
        $replacements = self::retrieveReplacements($raw);
229
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
230
231
        $shebang = self::retrieveShebang($raw);
232
233
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
234
235
        $stubBanner = self::retrieveStubBanner($raw);
236
        $stubBannerPath = self::retrieveStubBannerPath($raw);
237
        $stubBannerFromFile = self::retrieveStubBannerFromFile($basePath, $stubBannerPath);
238
239
        $stubPath = self::retrieveStubPath($raw);
240
241
        $isExtractable = self::retrieveIsExtractable($raw);
242
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
243
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
244
        $isWebPhar = self::retrieveIsWebPhar($raw);
245
246
        self::$fileSystem = null;
247
248
        return new self(
249
            $alias,
250
            $basePathRetriever,
251
            $filesAggregate,
252
            $binaryFilesAggregate,
253
            $bootstrapFile,
254
            $compactors,
255
            $compressionAlgorithm,
256
            $fileMode,
257
            $mainScriptPath,
258
            $mainScriptContent,
259
            $fileMapper,
260
            $metadata,
261
            $mimeTypeMapping,
262
            $mungVariables,
263
            $notFoundScriptPath,
264
            $outputPath,
265
            $privateKeyPassphrase,
266
            $privateKeyPath,
267
            $isPrivateKeyPrompt,
268
            $processedReplacements,
269
            $shebang,
270
            $signingAlgorithm,
271
            $stubBanner,
272
            $stubBannerPath,
273
            $stubBannerFromFile,
274
            $stubPath,
275
            $isExtractable,
276
            $isInterceptFileFuncs,
277
            $isStubGenerated,
278
            $isWebPhar
279
        );
280
    }
281
282
    public function getBasePathRetriever(): RetrieveRelativeBasePath
283
    {
284
        return $this->basePathRetriever;
285
    }
286
287
    public function getAlias(): string
288
    {
289
        return $this->alias;
290
    }
291
292
    public function getBasePath(): string
293
    {
294
        return $this->basePathRetriever->getBasePath();
295
    }
296
297
    /**
298
     * @return SplFileInfo[]
299
     */
300
    public function getFiles(): array
301
    {
302
        return $this->files;
303
    }
304
305
    /**
306
     * @return SplFileInfo[]
307
     */
308
    public function getBinaryFiles(): array
309
    {
310
        return $this->binaryFiles;
311
    }
312
313
    public function getBootstrapFile(): ?string
314
    {
315
        return $this->bootstrapFile;
316
    }
317
318
    public function loadBootstrap(): void
319
    {
320
        $file = $this->bootstrapFile;
321
322
        if (null !== $file) {
323
            include $file;
324
        }
325
    }
326
327
    /**
328
     * @return Compactor[] the list of compactors
329
     */
330
    public function getCompactors(): array
331
    {
332
        return $this->compactors;
333
    }
334
335
    public function getCompressionAlgorithm(): ?int
336
    {
337
        return $this->compressionAlgorithm;
338
    }
339
340
    public function getFileMode(): ?int
341
    {
342
        return $this->fileMode;
343
    }
344
345
    public function getMainScriptPath(): ?string
346
    {
347
        return $this->mainScriptPath;
348
    }
349
350
    public function getMainScriptContent(): ?string
351
    {
352
        return $this->mainScriptContent;
353
    }
354
355
    public function getMimetypeMapping(): array
356
    {
357
        return $this->mimetypeMapping;
358
    }
359
360
    public function getMungVariables(): array
361
    {
362
        return $this->mungVariables;
363
    }
364
365
    public function getNotFoundScriptPath(): ?string
366
    {
367
        return $this->notFoundScriptPath;
368
    }
369
370
    public function getOutputPath(): string
371
    {
372
        return $this->outputPath;
373
    }
374
375
    /**
376
     * @return string[]
377
     */
378
    public function getMap(): array
379
    {
380
        return $this->fileMapper->getMap();
381
    }
382
383
    public function getFileMapper(): MapFile
384
    {
385
        return $this->fileMapper;
386
    }
387
388
    /**
389
     * @return mixed
390
     */
391
    public function getMetadata()
392
    {
393
        return $this->metadata;
394
    }
395
396
    public function getPrivateKeyPassphrase(): ?string
397
    {
398
        return $this->privateKeyPassphrase;
399
    }
400
401
    public function getPrivateKeyPath(): ?string
402
    {
403
        return $this->privateKeyPath;
404
    }
405
406
    public function isPrivateKeyPrompt(): bool
407
    {
408
        return $this->isPrivateKeyPrompt;
409
    }
410
411
    public function getProcessedReplacements(): array
412
    {
413
        return $this->processedReplacements;
414
    }
415
416
    public function getShebang(): ?string
417
    {
418
        return $this->shebang;
419
    }
420
421
    public function getSigningAlgorithm(): int
422
    {
423
        return $this->signingAlgorithm;
424
    }
425
426
    public function getStubBanner(): ?string
427
    {
428
        return $this->stubBanner;
429
    }
430
431
    public function getStubBannerPath(): ?string
432
    {
433
        return $this->stubBannerPath;
434
    }
435
436
    public function getStubBannerFromFile()
437
    {
438
        return $this->stubBannerFromFile;
439
    }
440
441
    public function getStubPath(): ?string
442
    {
443
        return $this->stubPath;
444
    }
445
446
    public function isExtractable(): bool
447
    {
448
        return $this->isExtractable;
449
    }
450
451
    public function isInterceptFileFuncs(): bool
452
    {
453
        return $this->isInterceptFileFuncs;
454
    }
455
456
    public function isStubGenerated(): bool
457
    {
458
        return $this->isStubGenerated;
459
    }
460
461
    public function isWebPhar(): bool
462
    {
463
        return $this->isWebPhar;
464
    }
465
466
    private static function retrieveAlias(stdClass $raw): string
467
    {
468
        $alias = $raw->alias ?? self::DEFAULT_ALIAS;
469
470
        $alias = trim($alias);
471
472
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty.');
473
474
        return $alias;
475
    }
476
477
    private static function retrieveBasePath(string $file, stdClass $raw): string
478
    {
479
        if (false === isset($raw->{'base-path'})) {
480
            return realpath(dirname($file));
481
        }
482
483
        $basePath = trim($raw->{'base-path'});
484
485
        Assertion::directory(
486
            $basePath,
487
            'The base path "%s" is not a directory or does not exist.'
488
        );
489
490
        return realpath($basePath);
491
    }
492
493
    /**
494
     * @param stdClass $raw
495
     * @param string   $basePath
496
     *
497
     * @return Closure
498
     */
499
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
500
    {
501
        $blacklist = self::retrieveBlacklist($raw, $basePath);
502
503
        return function (SplFileInfo $file) use ($blacklist): ?bool {
504
            if (in_array($file->getRealPath(), $blacklist, true)) {
505
                return false;
506
            }
507
508
            return null;
509
        };
510
    }
511
512
    /**
513
     * @return string[]
514
     */
515
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
516
    {
517
        if (false === isset($raw->blacklist)) {
518
            return [];
519
        }
520
521
        $blacklist = $raw->blacklist;
522
523
        $normalizePath = function ($file) use ($basePath): string {
524
            $file = trim($file);
525
526
            if (false === self::$fileSystem->isAbsolutePath($file)) {
527
                $file = $basePath.DIRECTORY_SEPARATOR.canonicalize($file);
528
            }
529
530
            return $file;
531
        };
532
533
        return array_map($normalizePath, $blacklist);
534
    }
535
536
    /**
537
     * @param stdClass $raw
538
     * @param string   $key      Config property name
539
     * @param string   $basePath
540
     *
541
     * @return SplFileInfo[]
542
     */
543
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
544
    {
545
        if (false === isset($raw->{$key})) {
546
            return [];
547
        }
548
549
        $files = (array) $raw->{$key};
550
551
        Assertion::allString($files);
552
553
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
554
            $file = trim($file);
555
556
            if (false === self::$fileSystem->isAbsolutePath($file)) {
557
                $file = $basePath.DIRECTORY_SEPARATOR.canonicalize($file);
558
            }
559
560
            Assertion::file(
561
                $file,
562
                sprintf(
563
                    '"%s" must contain a list of existing files. Could not find "%%s".',
564
                    $key
565
                )
566
            );
567
568
            return new SplFileInfo($file);
569
        };
570
571
        return array_map($normalizePath, $files);
572
    }
573
574
    /**
575
     * @param stdClass $raw
576
     * @param string   $key             Config property name
577
     * @param string   $basePath
578
     * @param Closure  $blacklistFilter
579
     *
580
     * @return iterable|SplFileInfo[]
581
     */
582
    private static function retrieveDirectories(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): iterable
583
    {
584
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
585
586
        if ([] !== $directories) {
587
            return Finder::create()
588
                ->files()
589
                ->filter($blacklistFilter)
590
                ->ignoreVCS(true)
591
                ->in($directories)
592
            ;
593
        }
594
595
        return [];
596
    }
597
598
    /**
599
     * @param stdClass $raw
600
     * @param string   $basePath
601
     * @param Closure  $blacklistFilter
602
     *
603
     * @return iterable[]|SplFileInfo[][]
604
     */
605
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
606
    {
607
        if (isset($raw->{$key})) {
608
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
609
        }
610
611
        return [];
612
    }
613
614
    /**
615
     * @param array   $findersConfig   the configuration
616
     * @param string  $basePath
617
     * @param Closure $blacklistFilter
618
     *
619
     * @return iterable[]|SplFileInfo[][]
620
     */
621
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
622
    {
623
        $processFinderConfig = function ($methods) use ($basePath, $blacklistFilter) {
624
            $finder = Finder::create()
625
                ->files()
626
                ->filter($blacklistFilter)
627
                ->ignoreVCS(true)
628
            ;
629
630
            // TODO: refactor those check functions once https://github.com/symfony/symfony/issues/25945#issuecomment-361002235 is clarified
631
            $createNormalizeDirectory = function (bool $checkIfExists) use ($basePath): Closure {
632
                return function (string &$directory) use ($checkIfExists, $basePath): void {
633
                    $directory = trim($directory);
634
635
                    if (false === self::$fileSystem->isAbsolutePath($directory)) {
636
                        $directory = sprintf(
637
                            '%s%s',
638
                            $basePath.DIRECTORY_SEPARATOR,
639
                            rtrim(
640
                                canonicalize($directory),
641
                                DIRECTORY_SEPARATOR
642
                            )
643
                        );
644
                    }
645
646
                    if ($checkIfExists) {
647
                        Assertion::directory($directory);
648
                    }
649
                };
650
            };
651
652
            $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
653
                $fileOrDirectory = trim($fileOrDirectory);
654
655
                if (false === self::$fileSystem->isAbsolutePath($fileOrDirectory)) {
656
                    $fileOrDirectory = sprintf(
657
                        '%s%s',
658
                        $basePath.DIRECTORY_SEPARATOR,
659
                        rtrim(
660
                            canonicalize($fileOrDirectory),
661
                            DIRECTORY_SEPARATOR
662
                        )
663
                    );
664
                }
665
666
                if (false === file_exists($fileOrDirectory)) {
667
                    throw new InvalidArgumentException(
668
                        sprintf(
669
                            'Path "%s" was expected to be a file or directory.',
670
                            $fileOrDirectory
671
                        )
672
                    );
673
                }
674
675
                if (false === is_file($fileOrDirectory)) {
676
                    Assertion::directory($fileOrDirectory);
677
                } else {
678
                    Assertion::file($fileOrDirectory);
679
                }
680
            };
681
682
            foreach ($methods as $method => $arguments) {
683
                $method = trim($method);
684
                $arguments = (array) $arguments;
685
686
                if (false === method_exists($finder, $method)) {
687
                    throw new InvalidArgumentException(
688
                        sprintf(
689
                            'The method "Finder::%s" does not exist.',
690
                            $method
691
                        )
692
                    );
693
                }
694
695
                if ('in' === $method) {
696
                    array_walk($arguments, $createNormalizeDirectory(true));
697
                }
698
699
                if ('exclude' === $method) {
700
                    array_walk($arguments, $createNormalizeDirectory(false));
701
                }
702
703
                if ('append' === $method) {
704
                    array_walk($arguments, $normalizeFileOrDirectory);
705
706
                    $arguments = [$arguments];
707
                }
708
709
                foreach ($arguments as $argument) {
710
                    $finder->$method($argument);
711
                }
712
            }
713
714
            return $finder;
715
        };
716
717
        return array_map($processFinderConfig, $findersConfig);
718
    }
719
720
    /**
721
     * @param stdClass $raw
722
     * @param string   $key      Config property name
723
     * @param string   $basePath
724
     *
725
     * @return string[]
726
     */
727
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
728
    {
729
        if (false === isset($raw->{$key})) {
730
            return [];
731
        }
732
733
        $directories = $raw->{$key};
734
735
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
736
            $directory = trim($directory);
737
738
            if (false === self::$fileSystem->isAbsolutePath($directory)) {
739
                $directory = sprintf(
740
                    '%s%s',
741
                    $basePath.DIRECTORY_SEPARATOR,
742
                    rtrim(
743
                        canonicalize($directory),
744
                        DIRECTORY_SEPARATOR
745
                    )
746
                );
747
            }
748
749
            Assertion::directory(
750
                $directory,
751
                sprintf(
752
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
753
                    $key
754
                )
755
            );
756
757
            return $directory;
758
        };
759
760
        return array_map($normalizeDirectory, $directories);
761
    }
762
763
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
764
    {
765
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
766
        // through that extension point so this is pretty much useless unless proven otherwise.
767
        if (false === isset($raw->bootstrap)) {
768
            return null;
769
        }
770
771
        $file = $raw->bootstrap;
772
773
        if (false === is_absolute($file)) {
774
            $file = canonicalize(
775
                $basePath.DIRECTORY_SEPARATOR.$file
776
            );
777
        }
778
779
        if (false === file_exists($file)) {
780
            throw new InvalidArgumentException(
781
                sprintf(
782
                    'The bootstrap path "%s" is not a file or does not exist.',
783
                    $file
784
                )
785
            );
786
        }
787
788
        return $file;
789
    }
790
791
    /**
792
     * @return Compactor[]
793
     */
794
    private static function retrieveCompactors(stdClass $raw): array
795
    {
796
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
797
        if (false === isset($raw->compactors)) {
798
            return [];
799
        }
800
801
        $compactors = [];
802
803
        foreach ((array) $raw->compactors as $class) {
804
            if (false === class_exists($class)) {
805
                throw new InvalidArgumentException(
806
                    sprintf(
807
                        'The compactor class "%s" does not exist.',
808
                        $class
809
                    )
810
                );
811
            }
812
813
            if (Php::class === $class || LegacyPhp::class === $class) {
814
                $compactor = self::createPhpCompactor($raw);
815
            } else {
816
                $compactor = new $class();
817
            }
818
819
            if (false === ($compactor instanceof Compactor)) {
820
                throw new InvalidArgumentException(
821
                    sprintf(
822
                        'The class "%s" is not a compactor class.',
823
                        $class
824
                    )
825
                );
826
            }
827
828
            $compactors[] = $compactor;
829
        }
830
831
        return $compactors;
832
    }
833
834
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
835
    {
836
        // TODO: if in dev mode (when added), do not comment about the compression.
837
        // If not, add a warning to notify the user if no compression algorithm is used
838
        // provided the PHAR is not configured for web purposes.
839
        // If configured for the web, add a warning when a compression algorithm is used
840
        // as this can result in an overhead. Add a doc link explaining this.
841
        //
842
        // Unlike the doc: do not accept integers and document this BC break.
843
        if (false === isset($raw->compression)) {
844
            return null;
845
        }
846
847
        if (false === is_string($raw->compression)) {
848
            Assertion::integer(
849
                $raw->compression,
850
                'Expected compression to be an algorithm name, found %s instead.'
851
            );
852
853
            return $raw->compression;
854
        }
855
856
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
857
858
        Assertion::inArray(
859
            $raw->compression,
860
            $knownAlgorithmNames,
861
            sprintf(
862
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
863
                implode('", "', $knownAlgorithmNames)
864
            )
865
        );
866
867
        $value = get_phar_compression_algorithms()[$raw->compression];
868
869
        // Phar::NONE is not valid for compressFiles()
870
        if (Phar::NONE === $value) {
871
            return null;
872
        }
873
874
        return $value;
875
    }
876
877
    private static function retrieveFileMode(stdClass $raw): ?int
878
    {
879
        if (isset($raw->chmod)) {
880
            return intval($raw->chmod, 8);
881
        }
882
883
        return null;
884
    }
885
886
    private static function retrieveMainScriptPath(stdClass $raw): ?string
887
    {
888
        // TODO: check if is used for the web as well when web is set to true
889
        // If that the case make this field mandatory otherwise adjust the check
890
        // rules accordinly to ensure we do not have an empty PHAR
891
        if (isset($raw->main)) {
892
            return canonicalize($raw->main);
893
        }
894
895
        return null;
896
    }
897
898
    private static function retrieveMainScriptContents(?string $mainScriptPath, string $basePath): ?string
899
    {
900
        if (null === $mainScriptPath) {
901
            return null;
902
        }
903
904
        $mainScriptPath = $basePath.DIRECTORY_SEPARATOR.$mainScriptPath;
905
906
        Assertion::readable($mainScriptPath);
907
908
        $contents = file_get_contents($mainScriptPath);
909
910
        // Remove the shebang line
911
        return preg_replace('/^#!.*\s*/', '', $contents);
912
    }
913
914
    /**
915
     * @return string[][]
916
     */
917
    private static function retrieveMap(stdClass $raw): array
918
    {
919
        if (false === isset($raw->map)) {
920
            return [];
921
        }
922
923
        $map = [];
924
925
        foreach ((array) $raw->map as $item) {
926
            $processed = [];
927
928
            foreach ($item as $match => $replace) {
929
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
930
            }
931
932
            if (isset($processed['_empty_'])) {
933
                $processed[''] = $processed['_empty_'];
934
935
                unset($processed['_empty_']);
936
            }
937
938
            $map[] = $processed;
939
        }
940
941
        return $map;
942
    }
943
944
    /**
945
     * @return mixed
946
     */
947
    private static function retrieveMetadata(stdClass $raw)
948
    {
949
        // TODO: the doc currently say this can be any value; check if true
950
        // and if not add checks accordingly
951
        //
952
        // Also review the doc as I don't find it very helpful...
953
        if (isset($raw->metadata)) {
954
            if (is_object($raw->metadata)) {
955
                return (array) $raw->metadata;
956
            }
957
958
            return $raw->metadata;
959
        }
960
961
        return null;
962
    }
963
964
    private static function retrieveMimetypeMapping(stdClass $raw): array
965
    {
966
        // TODO: this parameter is not clear to me: review usage, doc & checks
967
        if (isset($raw->mimetypes)) {
968
            return (array) $raw->mimetypes;
969
        }
970
971
        return [];
972
    }
973
974
    private static function retrieveMungVariables(stdClass $raw): array
975
    {
976
        // TODO: this parameter is not clear to me: review usage, doc & checks
977
        // TODO: add error/warning if used when web is not enabled
978
        if (isset($raw->mung)) {
979
            return (array) $raw->mung;
980
        }
981
982
        return [];
983
    }
984
985
    private static function retrieveNotFoundScriptPath(stdClass $raw): ?string
986
    {
987
        // TODO: this parameter is not clear to me: review usage, doc & checks
988
        // TODO: add error/warning if used when web is not enabled
989
        if (isset($raw->{'not-found'})) {
990
            return $raw->{'not-found'};
991
        }
992
993
        return null;
994
    }
995
996
    private static function retrieveOutputPath(stdClass $raw, string $file): string
997
    {
998
        // TODO: make this path relative to the base path like everything else
999
        // otherwise this is really confusing. This is a BC break that needs to be
1000
        // documented though (and update the doc accordingly as well)
1001
        $base = getcwd().DIRECTORY_SEPARATOR;
1002
1003
        if (isset($raw->output)) {
1004
            $path = $raw->output;
1005
1006
            if (false === is_absolute($path)) {
1007
                $path = canonicalize($base.$path);
1008
            }
1009
        } else {
1010
            $path = $base.self::DEFAULT_ALIAS;
1011
        }
1012
1013
        if (false !== strpos($path, '@'.'git-version@')) {
1014
            $gitVersion = self::retrieveGitVersion($file);
1015
1016
            $path = str_replace('@'.'git-version@', $gitVersion, $path);
1017
        }
1018
1019
        return $path;
1020
    }
1021
1022
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1023
    {
1024
        // TODO: add check to not allow this setting without the private key path
1025
        if (isset($raw->{'key-pass'})
1026
            && is_string($raw->{'key-pass'})
1027
        ) {
1028
            return $raw->{'key-pass'};
1029
        }
1030
1031
        return null;
1032
    }
1033
1034
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1035
    {
1036
        // TODO: If passed need to check its existence
1037
        // Also need
1038
1039
        if (isset($raw->key)) {
1040
            return $raw->key;
1041
        }
1042
1043
        return null;
1044
    }
1045
1046
    private static function retrieveReplacements(stdClass $raw): array
1047
    {
1048
        // TODO: add exmample in the doc
1049
        // Add checks against the values
1050
        if (isset($raw->replacements)) {
1051
            return (array) $raw->replacements;
1052
        }
1053
1054
        return [];
1055
    }
1056
1057
    private static function retrieveProcessedReplacements(
1058
        array $replacements,
1059
        stdClass $raw,
1060
        string $file
1061
    ): array {
1062
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1063
            $replacements[$git] = self::retrieveGitHash($file);
1064
        }
1065
1066
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1067
            $replacements[$git] = self::retrieveGitHash($file, true);
1068
        }
1069
1070
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1071
            $replacements[$git] = self::retrieveGitTag($file);
1072
        }
1073
1074
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1075
            $replacements[$git] = self::retrieveGitVersion($file);
1076
        }
1077
1078
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1079
            $replacements[$date] = self::retrieveDatetimeNow(
1080
                self::retrieveDatetimeFormat($raw)
1081
            );
1082
        }
1083
1084
        $sigil = self::retrieveReplacementSigil($raw);
1085
1086
        foreach ($replacements as $key => $value) {
1087
            unset($replacements[$key]);
1088
            $replacements["$sigil$key$sigil"] = $value;
1089
        }
1090
1091
        return $replacements;
1092
    }
1093
1094
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1095
    {
1096
        if (isset($raw->{'git-commit'})) {
1097
            return $raw->{'git-commit'};
1098
        }
1099
1100
        return null;
1101
    }
1102
1103
    /**
1104
     * @param string $file
1105
     * @param bool   $short Use the short version
1106
     *
1107
     * @return string the commit hash
1108
     */
1109
    private static function retrieveGitHash(string $file, bool $short = false): string
1110
    {
1111
        return self::runGitCommand(
1112
            sprintf(
1113
                'git log --pretty="%s" -n1 HEAD',
1114
                $short ? '%h' : '%H'
1115
            ),
1116
            $file
1117
        );
1118
    }
1119
1120
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1121
    {
1122
        if (isset($raw->{'git-commit-short'})) {
1123
            return $raw->{'git-commit-short'};
1124
        }
1125
1126
        return null;
1127
    }
1128
1129
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1130
    {
1131
        if (isset($raw->{'git-tag'})) {
1132
            return $raw->{'git-tag'};
1133
        }
1134
1135
        return null;
1136
    }
1137
1138
    private static function retrieveGitTag(string $file): ?string
1139
    {
1140
        return self::runGitCommand('git describe --tags HEAD', $file);
1141
    }
1142
1143
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1144
    {
1145
        if (isset($raw->{'git-version'})) {
1146
            return $raw->{'git-version'};
1147
        }
1148
1149
        return null;
1150
    }
1151
1152
    private static function retrieveGitVersion(string $file): ?string
1153
    {
1154
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1155
        // to avoid messing around with that
1156
1157
        try {
1158
            return self::retrieveGitTag($file);
1159
        } catch (RuntimeException $exception) {
1160
            try {
1161
                return self::retrieveGitHash($file, true);
1162
            } catch (RuntimeException $exception) {
1163
                throw new RuntimeException(
1164
                    sprintf(
1165
                        'The tag or commit hash could not be retrieved from "%s": %s',
1166
                        dirname($file),
1167
                        $exception->getMessage()
1168
                    ),
1169
                    0,
1170
                    $exception
1171
                );
1172
            }
1173
        }
1174
    }
1175
1176
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1177
    {
1178
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1179
        // Also make sure the documentation is up to date after.
1180
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1181
        // be better to have only one element IMO like:
1182
        //
1183
        // "datetime": {
1184
        //   "value": "val",
1185
        //   "format": "Y-m-d"
1186
        // }
1187
        //
1188
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1189
        // if the datetime format is the default one it's ok; but in any case the format should not
1190
        // be added without the datetime value...
1191
1192
        if (isset($raw->{'datetime'})) {
1193
            return $raw->{'datetime'};
1194
        }
1195
1196
        return null;
1197
    }
1198
1199
    private static function retrieveDatetimeNow(string $format)
1200
    {
1201
        $now = new DateTimeImmutable('now');
1202
1203
        $datetime = $now->format($format);
1204
1205
        if (!$datetime) {
1206
            throw new InvalidArgumentException(
1207
                sprintf(
1208
                    '""%s" is not a valid PHP date format',
1209
                    $format
1210
                )
1211
            );
1212
        }
1213
1214
        return $datetime;
1215
    }
1216
1217
    private static function retrieveDatetimeFormat(stdClass $raw): string
1218
    {
1219
        if (isset($raw->{'datetime_format'})) {
1220
            return $raw->{'datetime_format'};
1221
        }
1222
1223
        return self::DEFAULT_DATETIME_FORMAT;
1224
    }
1225
1226
    private static function retrieveReplacementSigil(stdClass $raw)
1227
    {
1228
        if (isset($raw->{'replacement-sigil'})) {
1229
            return $raw->{'replacement-sigil'};
1230
        }
1231
1232
        return self::DEFAULT_REPLACEMENT_SIGIL;
1233
    }
1234
1235
    private static function retrieveShebang(stdClass $raw): ?string
1236
    {
1237
        // TODO: unlike the doc says do not allow empty strings.
1238
        // Leverage `Assertion` here?
1239
        if (false === isset($raw->shebang)) {
1240
            return null;
1241
        }
1242
1243
        if (('' === $raw->shebang) || (false === $raw->shebang)) {
1244
            return '';
1245
        }
1246
1247
        $shebang = trim($raw->shebang);
1248
1249
        if ('#!' !== substr($shebang, 0, 2)) {
1250
            throw new InvalidArgumentException(
1251
                sprintf(
1252
                    'The shebang line must start with "#!": %s',
1253
                    $shebang
1254
                )
1255
            );
1256
        }
1257
1258
        return $shebang;
1259
    }
1260
1261
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1262
    {
1263
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1264
        // TODO: trigger a warning if the signing algorithm used is weak
1265
        // TODO: no longer accept strings & document BC break
1266
        if (false === isset($raw->algorithm)) {
1267
            return Phar::SHA1;
1268
        }
1269
1270
        if (is_string($raw->algorithm)) {
1271
            if (false === defined('Phar::'.$raw->algorithm)) {
1272
                throw new InvalidArgumentException(
1273
                    sprintf(
1274
                        'The signing algorithm "%s" is not supported.',
1275
                        $raw->algorithm
1276
                    )
1277
                );
1278
            }
1279
1280
            return constant('Phar::'.$raw->algorithm);
1281
        }
1282
1283
        return $raw->algorithm;
1284
    }
1285
1286
    private static function retrieveStubBanner(stdClass $raw): ?string
1287
    {
1288
        if (isset($raw->{'banner'})) {
1289
            return $raw->{'banner'};
1290
        }
1291
1292
        return null;
1293
    }
1294
1295
    private static function retrieveStubBannerPath(stdClass $raw): ?string
1296
    {
1297
        // TODO: if provided check its existence here or should it be defered to later?
1298
        // Works case this check can be duplicated...
1299
        //
1300
        // Check if is relative to base path: if not make it so (may be a BC break to document).
1301
        // Once checked, a mention in the doc that this path is relative to base-path (unless
1302
        // absolute).
1303
        // Check that the path is not provided if a banner is already provided.
1304
        if (isset($raw->{'banner-file'})) {
1305
            return canonicalize($raw->{'banner-file'});
1306
        }
1307
1308
        return null;
1309
    }
1310
1311
    private static function retrieveStubBannerFromFile(string $basePath, ?string $stubBannerPath): ?string
1312
    {
1313
        // TODO: Add checks
1314
        // TODO: The documentation is not clear enough IMO
1315
        if (null == $stubBannerPath) {
1316
            return null;
1317
        }
1318
1319
        $stubBannerPath = $basePath.DIRECTORY_SEPARATOR.$stubBannerPath;
1320
1321
        if (false === ($contents = @file_get_contents($stubBannerPath))) {
1322
            $errors = error_get_last();
1323
1324
            if (null === $errors) {
1325
                $errors = ['message' => 'failed to get contents of "'.$stubBannerPath.'""'];
1326
            }
1327
1328
            throw new InvalidArgumentException($errors['message']);
1329
        }
1330
1331
        return $contents;
1332
    }
1333
1334
    private static function retrieveStubPath(stdClass $raw): ?string
1335
    {
1336
        if (isset($raw->stub) && is_string($raw->stub)) {
1337
            return $raw->stub;
1338
        }
1339
1340
        return null;
1341
    }
1342
1343
    private static function retrieveIsExtractable(stdClass $raw): bool
1344
    {
1345
        // TODO: look it up, really not clear to me neither is the doc
1346
        if (isset($raw->extract)) {
1347
            return $raw->extract;
1348
        }
1349
1350
        return false;
1351
    }
1352
1353
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1354
    {
1355
        if (isset($raw->intercept)) {
1356
            return $raw->intercept;
1357
        }
1358
1359
        return false;
1360
    }
1361
1362
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1363
    {
1364
        if (isset($raw->{'key-pass'})
1365
            && (true === $raw->{'key-pass'})) {
1366
            return true;
1367
        }
1368
1369
        return false;
1370
    }
1371
1372
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1373
    {
1374
        if (isset($raw->stub) && (true === $raw->stub)) {
1375
            return true;
1376
        }
1377
1378
        return false;
1379
    }
1380
1381
    private static function retrieveIsWebPhar(stdClass $raw): bool
1382
    {
1383
        // TODO: doc is not clear enough
1384
        // Also check if is compatible web + CLI
1385
        if (isset($raw->web)) {
1386
            return $raw->web;
1387
        }
1388
1389
        return false;
1390
    }
1391
1392
    /**
1393
     * Runs a Git command on the repository.
1394
     *
1395
     * @param string $command the command
1396
     *
1397
     * @return string the trimmed output from the command
1398
     */
1399
    private static function runGitCommand(string $command, string $file): string
1400
    {
1401
        $path = dirname($file);
1402
1403
        $process = new Process($command, $path);
1404
1405
        if (0 === $process->run()) {
1406
            return trim($process->getOutput());
1407
        }
1408
1409
        throw new RuntimeException(
1410
            sprintf(
1411
                'The tag or commit hash could not be retrieved from "%s": %s',
1412
                $path,
1413
                $process->getErrorOutput()
1414
            )
1415
        );
1416
    }
1417
1418
    private static function createPhpCompactor(stdClass $raw): Compactor
1419
    {
1420
        // TODO: false === not set; check & add test/doc
1421
        $tokenizer = new Tokenizer();
1422
1423
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1424
            $tokenizer->ignore(
1425
                (array) $raw->annotations->ignore
1426
            );
1427
        }
1428
1429
        return new Php($tokenizer);
1430
    }
1431
}
1432