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