Passed
Push — master ( b6bb22...3224b2 )
by Théo
02:20
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 59
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 59
rs 9.597
c 0
b 0
f 0
cc 1
eloc 30
nc 1
nop 24

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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