Passed
Pull Request — master (#77)
by Théo
03:56 queued 01:36
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 60
rs 9.5555
c 0
b 0
f 0
cc 1
eloc 30
nc 1
nop 25

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