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