Completed
Pull Request — master (#77)
by Théo
02:54
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 KevinGH\Box\Composer\ComposerConfiguration;
0 ignored issues
show
Bug introduced by
The type KevinGH\Box\Composer\ComposerConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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