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