Passed
Pull Request — master (#76)
by Théo
02:23
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 function array_key_exists;
18
use function array_map;
19
use function array_merge;
20
use function array_unique;
21
use Assert\Assertion;
22
use Closure;
23
use DateTimeImmutable;
24
use function file_exists;
25
use function file_get_contents;
26
use Herrera\Annotations\Tokenizer;
27
use Herrera\Box\Compactor\Php as LegacyPhp;
28
use InvalidArgumentException;
29
use function is_link;
30
use KevinGH\Box\Compactor\Php;
31
use KevinGH\Box\Composer\ComposerConfiguration;
32
use Phar;
33
use RuntimeException;
34
use SplFileInfo;
35
use stdClass;
36
use Symfony\Component\Finder\Finder;
37
use Symfony\Component\Process\Process;
38
use function iter\chain;
39
use function KevinGH\Box\FileSystem\canonicalize;
40
use function KevinGH\Box\FileSystem\file_contents;
41
use function KevinGH\Box\FileSystem\make_path_absolute;
42
use function KevinGH\Box\FileSystem\make_path_relative;
43
44
final class Configuration
45
{
46
    private const DEFAULT_ALIAS = 'default.phar';
47
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
48
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
49
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
50
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
51
    private const DEFAULT_BANNER = <<<'BANNER'
52
Generated by Humbug Box.
53
54
@link https://github.com/humbug/box
55
BANNER;
56
    private const FILES_SETTINGS = [
57
        'files',
58
        'files-bin',
59
        'directories',
60
        'directories-bin',
61
        'finder',
62
        'finder-bin',
63
    ];
64
65
    private $fileMode;
66
    private $alias;
67
    private $basePathRetriever;
68
    private $files;
69
    private $binaryFiles;
70
    private $bootstrapFile;
71
    private $compactors;
72
    private $compressionAlgorithm;
73
    private $mainScriptPath;
74
    private $mainScriptContents;
75
    private $map;
76
    private $fileMapper;
77
    private $metadata;
78
    private $outputPath;
79
    private $privateKeyPassphrase;
80
    private $privateKeyPath;
81
    private $isPrivateKeyPrompt;
82
    private $processedReplacements;
83
    private $shebang;
84
    private $signingAlgorithm;
85
    private $stubBannerContents;
86
    private $stubBannerPath;
87
    private $stubPath;
88
    private $isInterceptFileFuncs;
89
    private $isStubGenerated;
90
91
    /**
92
     * @param null|string              $file
93
     * @param null|string              $alias
94
     * @param RetrieveRelativeBasePath $basePathRetriever     Utility to private the base path used and be able to retrieve a path relative to it (the base path)
95
     * @param SplFileInfo[]            $files                 List of files
96
     * @param SplFileInfo[]            $binaryFiles           List of binary files
97
     * @param null|string              $bootstrapFile         The bootstrap file path
98
     * @param Compactor[]              $compactors            List of file contents compactors
99
     * @param null|int                 $compressionAlgorithm  Compression algorithm constant value. See the \Phar class constants
100
     * @param null|int                 $fileMode              File mode in octal form
101
     * @param string                   $mainScriptPath        The main script file path
102
     * @param string                   $mainScriptContents    The processed content of the main script file
103
     * @param MapFile                  $fileMapper            Utility to map the files from outside and inside the PHAR
104
     * @param mixed                    $metadata              The PHAR Metadata
105
     * @param string                   $outputPath
106
     * @param null|string              $privateKeyPassphrase
107
     * @param null|string              $privateKeyPath
108
     * @param bool                     $isPrivateKeyPrompt    If the user should be prompted for the private key passphrase
109
     * @param array                    $processedReplacements The processed list of replacement placeholders and their values
110
     * @param null|string              $shebang               The shebang line
111
     * @param int                      $signingAlgorithm      The PHAR siging algorithm. See \Phar constants
112
     * @param null|string              $stubBannerContents    The stub banner comment
113
     * @param null|string              $stubBannerPath        The path to the stub banner comment file
114
     * @param null|string              $stubPath              The PHAR stub file path
115
     * @param bool                     $isInterceptFileFuncs  wether or not Phar::interceptFileFuncs() should be used
116
     * @param bool                     $isStubGenerated       Wether or not if the PHAR stub should be generated
117
     */
118
    private function __construct(
119
        ?string $file,
120
        ?string $alias,
121
        RetrieveRelativeBasePath $basePathRetriever,
122
        array $files,
123
        array $binaryFiles,
124
        ?string $bootstrapFile,
125
        array $compactors,
126
        ?int $compressionAlgorithm,
127
        ?int $fileMode,
128
        string $mainScriptPath,
129
        string $mainScriptContents,
130
        MapFile $fileMapper,
131
        $metadata,
132
        string $outputPath,
133
        ?string $privateKeyPassphrase,
134
        ?string $privateKeyPath,
135
        bool $isPrivateKeyPrompt,
136
        array $processedReplacements,
137
        ?string $shebang,
138
        int $signingAlgorithm,
139
        ?string $stubBannerContents,
140
        ?string $stubBannerPath,
141
        ?string $stubPath,
142
        bool $isInterceptFileFuncs,
143
        bool $isStubGenerated
144
    ) {
145
        Assertion::nullOrInArray(
146
            $compressionAlgorithm,
147
            get_phar_compression_algorithms(),
148
            sprintf(
149
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
150
                implode('", "', array_keys(get_phar_compression_algorithms()))
151
            )
152
        );
153
154
        $this->alias = $alias;
155
        $this->basePathRetriever = $basePathRetriever;
156
        $this->files = $files;
157
        $this->binaryFiles = $binaryFiles;
158
        $this->bootstrapFile = $bootstrapFile;
159
        $this->compactors = $compactors;
160
        $this->compressionAlgorithm = $compressionAlgorithm;
161
        $this->fileMode = $fileMode;
162
        $this->mainScriptPath = $mainScriptPath;
163
        $this->mainScriptContents = $mainScriptContents;
164
        $this->fileMapper = $fileMapper;
165
        $this->metadata = $metadata;
166
        $this->outputPath = $outputPath;
167
        $this->privateKeyPassphrase = $privateKeyPassphrase;
168
        $this->privateKeyPath = $privateKeyPath;
169
        $this->isPrivateKeyPrompt = $isPrivateKeyPrompt;
170
        $this->processedReplacements = $processedReplacements;
171
        $this->shebang = $shebang;
172
        $this->signingAlgorithm = $signingAlgorithm;
173
        $this->stubBannerContents = $stubBannerContents;
174
        $this->stubBannerPath = $stubBannerPath;
175
        $this->stubPath = $stubPath;
176
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
177
        $this->isStubGenerated = $isStubGenerated;
178
    }
179
180
    public static function create(?string $file, stdClass $raw): self
181
    {
182
        $alias = self::retrieveAlias($raw);
183
184
        $basePath = self::retrieveBasePath($file, $raw);
185
        $basePathRetriever = new RetrieveRelativeBasePath($basePath);
186
187
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath);
188
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
189
190
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath);
191
192
        $blacklistFilter = self::retrieveBlacklistFilter($raw, $basePath);
193
194
        if (self::shouldRetrieveAllFiles($file, $raw)) {
195
            $filesAggregate = self::retrieveAllFiles($basePath, $mainScriptPath, $blacklistFilter, $devPackages);
196
            $binaryFilesAggregate = [];
197
        } else {
198
            $files = self::retrieveFiles($raw, 'files', $basePath);
199
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);
0 ignored issues
show
Bug introduced by
The call to KevinGH\Box\Configuration::retrieveDirectories() has too few arguments starting with devPackages. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

199
            /** @scrutinizer ignore-call */ 
200
            $directories = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
200
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter);
201
202
            $filesAggregate = array_unique(iterator_to_array(chain($files, $directories, ...$filesFromFinders)));
203
204
            $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath);
205
            $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter);
206
            $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter);
207
208
            $binaryFilesAggregate = array_unique(iterator_to_array(chain($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders)));
209
        }
210
211
        $bootstrapFile = self::retrieveBootstrapFile($raw, $basePath);
212
213
        $compactors = self::retrieveCompactors($raw);
214
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
215
216
        $fileMode = self::retrieveFileMode($raw);
217
218
        $map = self::retrieveMap($raw);
219
        $fileMapper = new MapFile($map);
220
221
        $metadata = self::retrieveMetadata($raw);
222
223
        $outputPath = self::retrieveOutputPath($raw, $basePath);
224
225
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw);
226
        $privateKeyPath = self::retrievePrivateKeyPath($raw);
227
        $isPrivateKeyPrompt = self::retrieveIsPrivateKeyPrompt($raw);
228
229
        $replacements = self::retrieveReplacements($raw);
230
        $processedReplacements = self::retrieveProcessedReplacements($replacements, $raw, $file);
231
232
        $shebang = self::retrieveShebang($raw);
233
234
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
235
236
        $stubBannerContents = self::retrieveStubBannerContents($raw);
237
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
238
239
        if (null !== $stubBannerPath) {
240
            $stubBannerContents = file_contents($stubBannerPath);
241
        }
242
243
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
244
245
        $stubPath = self::retrieveStubPath($raw, $basePath);
246
247
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
248
        $isStubGenerated = self::retrieveIsStubGenerated($raw);
249
250
        return new self(
251
            $file,
252
            $alias,
253
            $basePathRetriever,
254
            $filesAggregate,
255
            $binaryFilesAggregate,
256
            $bootstrapFile,
257
            $compactors,
258
            $compressionAlgorithm,
259
            $fileMode,
260
            $mainScriptPath,
261
            $mainScriptContents,
262
            $fileMapper,
263
            $metadata,
264
            $outputPath,
265
            $privateKeyPassphrase,
266
            $privateKeyPath,
267
            $isPrivateKeyPrompt,
268
            $processedReplacements,
269
            $shebang,
270
            $signingAlgorithm,
271
            $stubBannerContents,
272
            $stubBannerPath,
273
            $stubPath,
274
            $isInterceptFileFuncs,
275
            $isStubGenerated
276
        );
277
    }
278
279
    public function getBasePathRetriever(): RetrieveRelativeBasePath
280
    {
281
        return $this->basePathRetriever;
282
    }
283
284
    public function getAlias(): ?string
285
    {
286
        return $this->alias;
287
    }
288
289
    public function getBasePath(): string
290
    {
291
        return $this->basePathRetriever->getBasePath();
292
    }
293
294
    /**
295
     * @return string[]
296
     */
297
    public function getFiles(): array
298
    {
299
        return $this->files;
300
    }
301
302
    /**
303
     * @return string[]
304
     */
305
    public function getBinaryFiles(): array
306
    {
307
        return $this->binaryFiles;
308
    }
309
310
    public function getBootstrapFile(): ?string
311
    {
312
        return $this->bootstrapFile;
313
    }
314
315
    public function loadBootstrap(): void
316
    {
317
        $file = $this->bootstrapFile;
318
319
        if (null !== $file) {
320
            include $file;
321
        }
322
    }
323
324
    /**
325
     * @return Compactor[] the list of compactors
326
     */
327
    public function getCompactors(): array
328
    {
329
        return $this->compactors;
330
    }
331
332
    public function getCompressionAlgorithm(): ?int
333
    {
334
        return $this->compressionAlgorithm;
335
    }
336
337
    public function getFileMode(): ?int
338
    {
339
        return $this->fileMode;
340
    }
341
342
    public function getMainScriptPath(): string
343
    {
344
        return $this->mainScriptPath;
345
    }
346
347
    public function getMainScriptContents(): string
348
    {
349
        return $this->mainScriptContents;
350
    }
351
352
    public function getOutputPath(): string
353
    {
354
        return $this->outputPath;
355
    }
356
357
    /**
358
     * @return string[]
359
     */
360
    public function getMap(): array
361
    {
362
        return $this->fileMapper->getMap();
363
    }
364
365
    public function getFileMapper(): MapFile
366
    {
367
        return $this->fileMapper;
368
    }
369
370
    /**
371
     * @return mixed
372
     */
373
    public function getMetadata()
374
    {
375
        return $this->metadata;
376
    }
377
378
    public function getPrivateKeyPassphrase(): ?string
379
    {
380
        return $this->privateKeyPassphrase;
381
    }
382
383
    public function getPrivateKeyPath(): ?string
384
    {
385
        return $this->privateKeyPath;
386
    }
387
388
    public function isPrivateKeyPrompt(): bool
389
    {
390
        return $this->isPrivateKeyPrompt;
391
    }
392
393
    public function getProcessedReplacements(): array
394
    {
395
        return $this->processedReplacements;
396
    }
397
398
    public function getShebang(): ?string
399
    {
400
        return $this->shebang;
401
    }
402
403
    public function getSigningAlgorithm(): int
404
    {
405
        return $this->signingAlgorithm;
406
    }
407
408
    public function getStubBannerContents(): ?string
409
    {
410
        return $this->stubBannerContents;
411
    }
412
413
    public function getStubBannerPath(): ?string
414
    {
415
        return $this->stubBannerPath;
416
    }
417
418
    public function getStubPath(): ?string
419
    {
420
        return $this->stubPath;
421
    }
422
423
    public function isInterceptFileFuncs(): bool
424
    {
425
        return $this->isInterceptFileFuncs;
426
    }
427
428
    public function isStubGenerated(): bool
429
    {
430
        return $this->isStubGenerated;
431
    }
432
433
    private static function retrieveAlias(stdClass $raw): ?string
434
    {
435
        if (false === isset($raw->alias)) {
436
            return null;
437
        }
438
439
        $alias = trim($raw->alias);
440
441
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
442
443
        return $alias;
444
    }
445
446
    private static function retrieveBasePath(?string $file, stdClass $raw): string
447
    {
448
        if (null === $file) {
449
            return getcwd();
450
        }
451
452
        if (false === isset($raw->{'base-path'})) {
453
            return realpath(dirname($file));
454
        }
455
456
        $basePath = trim($raw->{'base-path'});
457
458
        Assertion::directory(
459
            $basePath,
460
            'The base path "%s" is not a directory or does not exist.'
461
        );
462
463
        return realpath($basePath);
464
    }
465
466
    private static function shouldRetrieveAllFiles(?string $file, stdClass $raw): bool
467
    {
468
        if (null === $file) {
469
            return true;
470
        }
471
472
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
473
        $rawConfig = (array) $raw;
474
475
        foreach (self::FILES_SETTINGS as $key) {
476
            if (array_key_exists($key, $rawConfig)) {
477
                return false;
478
            }
479
        }
480
481
        return true;
482
    }
483
484
    /**
485
     * @param stdClass $raw
486
     * @param string   $basePath
487
     *
488
     * @return Closure
489
     */
490
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath): Closure
491
    {
492
        $blacklist = self::retrieveBlacklist($raw, $basePath);
493
494
        return function (SplFileInfo $file) use ($blacklist): ?bool {
495
            if (in_array($file->getRealPath(), $blacklist, true)) {
496
                return false;
497
            }
498
499
            return null;
500
        };
501
    }
502
503
    /**
504
     * @param stdClass $raw
505
     * @param string $basePath
506
     *
507
     * @return string[]
508
     */
509
    private static function retrieveBlacklist(stdClass $raw, string $basePath): array
510
    {
511
        if (false === isset($raw->blacklist)) {
512
            return [];
513
        }
514
515
        $blacklist = $raw->blacklist;
516
517
        $normalizePath = function ($file) use ($basePath): string {
518
            return self::normalizeFilePath($file, $basePath);
519
        };
520
521
        return array_unique(array_map($normalizePath, $blacklist));
522
    }
523
524
    /**
525
     * @param stdClass $raw
526
     * @param string   $key      Config property name
527
     * @param string   $basePath
528
     *
529
     * @return SplFileInfo[]
530
     */
531
    private static function retrieveFiles(stdClass $raw, string $key, string $basePath): array
532
    {
533
        if (false === isset($raw->{$key})) {
534
            return [];
535
        }
536
537
        $files = (array) $raw->{$key};
538
539
        Assertion::allString($files);
540
541
        $normalizePath = function (string $file) use ($basePath, $key): SplFileInfo {
542
            $file = self::normalizeFilePath($file, $basePath);
543
544
            if (is_link($file)) {
545
                // TODO: add this to baberlei/assert
546
                throw new InvalidArgumentException(
547
                    sprintf(
548
                        'Cannot add the link "%s": links are not supported.',
549
                        $file
550
                    )
551
                );
552
            }
553
554
            Assertion::file(
555
                $file,
556
                sprintf(
557
                    '"%s" must contain a list of existing files. Could not find "%%s".',
558
                    $key
559
                )
560
            );
561
562
            return new SplFileInfo($file);
563
        };
564
565
        return array_map($normalizePath, $files);
566
    }
567
568
    /**
569
     * @param stdClass $raw
570
     * @param string   $key             Config property name
571
     * @param string   $basePath
572
     * @param string[]   $devPackages
573
     * @param Closure  $blacklistFilter
574
     *
575
     * @return iterable|SplFileInfo[]
576
     */
577
    private static function retrieveDirectories(
578
        stdClass $raw,
579
        string $key,
580
        string $basePath,
581
        Closure $blacklistFilter,
582
        array $devPackages
583
    ): iterable
584
    {
585
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
586
587
        if ([] !== $directories) {
588
            return Finder::create()
0 ignored issues
show
Bug Best Practice introduced by
The expression return Symfony\Component...ages)->in($directories) returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type iterable|SplFileInfo[].
Loading history...
589
                ->files()
590
                ->filter($blacklistFilter)
591
                ->ignoreVCS(true)
592
                ->exclude($devPackages)
593
                ->in($directories)
594
            ;
595
        }
596
597
        return [];
598
    }
599
600
    /**
601
     * @param stdClass $raw
602
     * @param string   $basePath
603
     * @param Closure  $blacklistFilter
604
     *
605
     * @return iterable[]|SplFileInfo[][]
606
     */
607
    private static function retrieveFilesFromFinders(stdClass $raw, string $key, string $basePath, Closure $blacklistFilter): array
608
    {
609
        if (isset($raw->{$key})) {
610
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter);
611
        }
612
613
        return [];
614
    }
615
616
    /**
617
     * @param array   $findersConfig
618
     * @param string  $basePath
619
     * @param Closure $blacklistFilter
620
     *
621
     * @return Finder[]|SplFileInfo[][]
622
     */
623
    private static function processFinders(array $findersConfig, string $basePath, Closure $blacklistFilter): array
624
    {
625
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter) {
626
            return self::processFinder($config, $basePath, $blacklistFilter);
0 ignored issues
show
Bug introduced by
The call to KevinGH\Box\Configuration::processFinder() has too few arguments starting with devPackages. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

626
            return self::/** @scrutinizer ignore-call */ processFinder($config, $basePath, $blacklistFilter);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
627
        };
628
629
        return array_map($processFinderConfig, $findersConfig);
630
    }
631
632
    /**
633
     * @param stdClass $config
634
     * @param string $basePath
635
     * @param Closure $blacklistFilter
636
     * @param string[] $devPackages
637
     *
638
     * @return Finder|SplFileInfo[]
639
     */
640
    private static function processFinder(stdClass $config, string $basePath, Closure $blacklistFilter, array $devPackages): Finder
641
    {
642
        $finder = Finder::create()
643
            ->files()
644
            ->filter($blacklistFilter)
645
            ->exclude($devPackages)
646
            ->ignoreVCS(true)
647
        ;
648
649
        $normalizedConfig = (function (array $config, Finder $finder): array {
650
            $normalizedConfig = [];
651
652
            foreach ($config as $method => $arguments) {
653
                $method = trim($method);
654
                $arguments = (array) $arguments;
655
656
                Assertion::methodExists(
657
                    $method,
658
                    $finder,
659
                    'The method "Finder::%s" does not exist.'
660
                );
661
662
                $normalizedConfig[$method] = $arguments;
663
            }
664
665
            krsort($normalizedConfig);
666
667
            return $normalizedConfig;
668
        })((array) $config, $finder);
669
670
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
671
            $directory = self::normalizeDirectoryPath($directory, $basePath);
672
673
            if (is_link($directory)) {
674
                // TODO: add this to baberlei/assert
675
                throw new InvalidArgumentException(
676
                    sprintf(
677
                        'Cannot append the link "%s" to the Finder: links are not supported.',
678
                        $directory
679
                    )
680
                );
681
            }
682
683
            Assertion::directory($directory);
684
685
            return $directory;
686
        };
687
688
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath): void {
689
            $fileOrDirectory = self::normalizeDirectoryPath($fileOrDirectory, $basePath);
690
691
            if (is_link($fileOrDirectory)) {
692
                // TODO: add this to baberlei/assert
693
                throw new InvalidArgumentException(
694
                    sprintf(
695
                        'Cannot append the link "%s" to the Finder: links are not supported.',
696
                        $fileOrDirectory
697
                    )
698
                );
699
            }
700
701
            // TODO: add this to baberlei/assert
702
            if (false === file_exists($fileOrDirectory)) {
703
                throw new InvalidArgumentException(
704
                    sprintf(
705
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
706
                        $fileOrDirectory
707
                    )
708
                );
709
            }
710
711
            //TODO: add fileExists (as file or directory) to Assert
712
            if (false === is_file($fileOrDirectory)) {
713
                Assertion::directory($fileOrDirectory);
714
            } else {
715
                Assertion::file($fileOrDirectory);
716
            }
717
        };
718
719
        foreach ($normalizedConfig as $method => $arguments) {
720
            if ('in' === $method) {
721
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
722
            }
723
724
            if ('exclude' === $method) {
725
                $arguments = array_unique(array_map('trim', $arguments));
726
            }
727
728
            if ('append' === $method) {
729
                array_walk($arguments, $normalizeFileOrDirectory);
730
731
                $arguments = [$arguments];
732
            }
733
734
            foreach ($arguments as $argument) {
735
                $finder->$method($argument);
736
            }
737
        }
738
739
        return $finder;
740
    }
741
742
    /**
743
     * @param string $basePath
744
     * @param string $mainScriptPath
745
     * @param Closure $blacklistFilter
746
     * @param string[] $devPackages
747
     *
748
     * @return SplFileInfo[]
749
     */
750
    private static function retrieveAllFiles(
751
        string $basePath,
752
        string $mainScriptPath,
753
        Closure $blacklistFilter,
754
        array $devPackages
755
    ): array
756
    {
757
        $relativeDevPackages = array_map(
758
            function (string $packagePath) use ($basePath): string {
759
                return make_path_relative($packagePath, $basePath);
760
            },
761
            $devPackages
762
        );
763
764
        $finder = Finder::create()
765
            ->files()
766
            ->in($basePath)
767
            ->notPath(make_path_relative($mainScriptPath, $basePath))
768
            ->filter($blacklistFilter)
769
            ->exclude($relativeDevPackages)
770
            ->ignoreVCS(true)
771
        ;
772
773
        return array_filter(
774
            array_unique(
775
                array_map(
776
                    function (SplFileInfo $fileInfo): ?string {
777
                        if (is_link((string) $fileInfo)) {
778
                            return null;
779
                        }
780
781
                        return false !== $fileInfo->getRealPath() ? $fileInfo->getRealPath() : null;
782
                    },
783
                    iterator_to_array(
784
                        $finder
785
                    )
786
                )
787
            )
788
        );
789
    }
790
791
    /**
792
     * @param stdClass $raw
793
     * @param string   $key      Config property name
794
     * @param string   $basePath
795
     *
796
     * @return string[]
797
     */
798
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
799
    {
800
        if (false === isset($raw->{$key})) {
801
            return [];
802
        }
803
804
        $directories = $raw->{$key};
805
806
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
807
            $directory = self::normalizeDirectoryPath($directory, $basePath);
808
809
            if (is_link($directory)) {
810
                // TODO: add this to baberlei/assert
811
                throw new InvalidArgumentException(
812
                    sprintf(
813
                        'Cannot add the link "%s": links are not supported.',
814
                        $directory
815
                    )
816
                );
817
            }
818
819
            Assertion::directory(
820
                $directory,
821
                sprintf(
822
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
823
                    $key
824
                )
825
            );
826
827
            return $directory;
828
        };
829
830
        return array_map($normalizeDirectory, $directories);
831
    }
832
833
    private static function normalizeFilePath(string $file, string $basePath): string
834
    {
835
        return make_path_absolute(trim($file), $basePath);
836
    }
837
838
    private static function normalizeDirectoryPath(string $directory, string $basePath): string
839
    {
840
        return make_path_absolute(trim($directory), $basePath);
841
    }
842
843
    private static function retrieveComposerLockFileContents(string $basePath): ?string
844
    {
845
        $composerLockFile = make_path_absolute('composer.lock', $basePath);
846
847
        return file_exists($composerLockFile) ? file_contents($composerLockFile) : null;
848
    }
849
850
    private static function retrieveBootstrapFile(stdClass $raw, string $basePath): ?string
851
    {
852
        // TODO: deprecate its usage & document this BC break. Compactors will not be configurable
853
        // through that extension point so this is pretty much useless unless proven otherwise.
854
        if (false === isset($raw->bootstrap)) {
855
            return null;
856
        }
857
858
        $file = self::normalizeFilePath($raw->bootstrap, $basePath);
859
860
        Assertion::file($file, 'The bootstrap path "%s" is not a file or does not exist.');
861
862
        return $file;
863
    }
864
865
    /**
866
     * @return Compactor[]
867
     */
868
    private static function retrieveCompactors(stdClass $raw): array
869
    {
870
        // TODO: only accept arrays when set unlike the doc says (it allows a string).
871
        if (false === isset($raw->compactors)) {
872
            return [];
873
        }
874
875
        $compactors = [];
876
877
        foreach ((array) $raw->compactors as $class) {
878
            Assertion::classExists($class, 'The compactor class "%s" does not exist.');
879
            Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
880
881
            if (Php::class === $class || LegacyPhp::class === $class) {
882
                $compactor = self::createPhpCompactor($raw);
883
            } else {
884
                $compactor = new $class();
885
            }
886
887
            $compactors[] = $compactor;
888
        }
889
890
        return $compactors;
891
    }
892
893
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
894
    {
895
        // TODO: if in dev mode (when added), do not comment about the compression.
896
        // If not, add a warning to notify the user if no compression algorithm is used
897
        // provided the PHAR is not configured for web purposes.
898
        // If configured for the web, add a warning when a compression algorithm is used
899
        // as this can result in an overhead. Add a doc link explaining this.
900
        //
901
        // Unlike the doc: do not accept integers and document this BC break.
902
        if (false === isset($raw->compression)) {
903
            return null;
904
        }
905
906
        if (false === is_string($raw->compression)) {
907
            Assertion::integer(
908
                $raw->compression,
909
                'Expected compression to be an algorithm name, found %s instead.'
910
            );
911
912
            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...
913
        }
914
915
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
916
917
        Assertion::inArray(
918
            $raw->compression,
919
            $knownAlgorithmNames,
920
            sprintf(
921
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
922
                implode('", "', $knownAlgorithmNames)
923
            )
924
        );
925
926
        $value = get_phar_compression_algorithms()[$raw->compression];
927
928
        // Phar::NONE is not valid for compressFiles()
929
        if (Phar::NONE === $value) {
930
            return null;
931
        }
932
933
        return $value;
934
    }
935
936
    private static function retrieveFileMode(stdClass $raw): ?int
937
    {
938
        if (isset($raw->chmod)) {
939
            return intval($raw->chmod, 8);
940
        }
941
942
        return null;
943
    }
944
945
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath): string
946
    {
947
        $main = isset($raw->main) ? $raw->main : self::DEFAULT_MAIN_SCRIPT;
948
949
        return self::normalizeFilePath($main, $basePath);
950
    }
951
952
    private static function retrieveMainScriptContents(string $mainScriptPath): string
953
    {
954
        $contents = file_contents($mainScriptPath);
955
956
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
957
        // PHAR entry point file.
958
        return preg_replace('/^#!.*\s*/', '', $contents);
959
    }
960
961
    /**
962
     * @return string[][]
963
     */
964
    private static function retrieveMap(stdClass $raw): array
965
    {
966
        if (false === isset($raw->map)) {
967
            return [];
968
        }
969
970
        $map = [];
971
972
        foreach ((array) $raw->map as $item) {
973
            $processed = [];
974
975
            foreach ($item as $match => $replace) {
976
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
977
            }
978
979
            if (isset($processed['_empty_'])) {
980
                $processed[''] = $processed['_empty_'];
981
982
                unset($processed['_empty_']);
983
            }
984
985
            $map[] = $processed;
986
        }
987
988
        return $map;
989
    }
990
991
    /**
992
     * @return mixed
993
     */
994
    private static function retrieveMetadata(stdClass $raw)
995
    {
996
        // TODO: the doc currently say this can be any value; check if true
997
        // and if not add checks accordingly
998
        //
999
        // Also review the doc as I don't find it very helpful...
1000
        if (isset($raw->metadata)) {
1001
            if (is_object($raw->metadata)) {
1002
                return (array) $raw->metadata;
1003
            }
1004
1005
            return $raw->metadata;
1006
        }
1007
1008
        return null;
1009
    }
1010
1011
    private static function retrieveOutputPath(stdClass $raw, string $basePath): string
1012
    {
1013
        if (isset($raw->output)) {
1014
            $path = $raw->output;
1015
        } else {
1016
            $path = self::DEFAULT_ALIAS;
1017
        }
1018
1019
        return make_path_absolute($path, $basePath);
1020
    }
1021
1022
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1023
    {
1024
        // TODO: add check to not allow this setting without the private key path
1025
        if (isset($raw->{'key-pass'})
1026
            && is_string($raw->{'key-pass'})
1027
        ) {
1028
            return $raw->{'key-pass'};
1029
        }
1030
1031
        return null;
1032
    }
1033
1034
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1035
    {
1036
        // TODO: If passed need to check its existence
1037
        // Also need
1038
1039
        if (isset($raw->key)) {
1040
            return $raw->key;
1041
        }
1042
1043
        return null;
1044
    }
1045
1046
    private static function retrieveReplacements(stdClass $raw): array
1047
    {
1048
        // TODO: add exmample in the doc
1049
        // Add checks against the values
1050
        if (isset($raw->replacements)) {
1051
            return (array) $raw->replacements;
1052
        }
1053
1054
        return [];
1055
    }
1056
1057
    private static function retrieveProcessedReplacements(
1058
        array $replacements,
1059
        stdClass $raw,
1060
        ?string $file
1061
    ): array {
1062
        if (null === $file) {
1063
            return [];
1064
        }
1065
1066
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1067
            $replacements[$git] = self::retrieveGitHash($file);
1068
        }
1069
1070
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1071
            $replacements[$git] = self::retrieveGitHash($file, true);
1072
        }
1073
1074
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1075
            $replacements[$git] = self::retrieveGitTag($file);
1076
        }
1077
1078
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1079
            $replacements[$git] = self::retrieveGitVersion($file);
1080
        }
1081
1082
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1083
            $replacements[$date] = self::retrieveDatetimeNow(
1084
                self::retrieveDatetimeFormat($raw)
1085
            );
1086
        }
1087
1088
        $sigil = self::retrieveReplacementSigil($raw);
1089
1090
        foreach ($replacements as $key => $value) {
1091
            unset($replacements[$key]);
1092
            $replacements["$sigil$key$sigil"] = $value;
1093
        }
1094
1095
        return $replacements;
1096
    }
1097
1098
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1099
    {
1100
        if (isset($raw->{'git-commit'})) {
1101
            return $raw->{'git-commit'};
1102
        }
1103
1104
        return null;
1105
    }
1106
1107
    /**
1108
     * @param string $file
1109
     * @param bool   $short Use the short version
1110
     *
1111
     * @return string the commit hash
1112
     */
1113
    private static function retrieveGitHash(string $file, bool $short = false): string
1114
    {
1115
        return self::runGitCommand(
1116
            sprintf(
1117
                'git log --pretty="%s" -n1 HEAD',
1118
                $short ? '%h' : '%H'
1119
            ),
1120
            $file
1121
        );
1122
    }
1123
1124
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1125
    {
1126
        if (isset($raw->{'git-commit-short'})) {
1127
            return $raw->{'git-commit-short'};
1128
        }
1129
1130
        return null;
1131
    }
1132
1133
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1134
    {
1135
        if (isset($raw->{'git-tag'})) {
1136
            return $raw->{'git-tag'};
1137
        }
1138
1139
        return null;
1140
    }
1141
1142
    private static function retrieveGitTag(string $file): ?string
1143
    {
1144
        return self::runGitCommand('git describe --tags HEAD', $file);
1145
    }
1146
1147
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1148
    {
1149
        if (isset($raw->{'git-version'})) {
1150
            return $raw->{'git-version'};
1151
        }
1152
1153
        return null;
1154
    }
1155
1156
    private static function retrieveGitVersion(string $file): ?string
1157
    {
1158
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1159
        // to avoid messing around with that
1160
1161
        try {
1162
            return self::retrieveGitTag($file);
1163
        } catch (RuntimeException $exception) {
1164
            try {
1165
                return self::retrieveGitHash($file, true);
1166
            } catch (RuntimeException $exception) {
1167
                throw new RuntimeException(
1168
                    sprintf(
1169
                        'The tag or commit hash could not be retrieved from "%s": %s',
1170
                        dirname($file),
1171
                        $exception->getMessage()
1172
                    ),
1173
                    0,
1174
                    $exception
1175
                );
1176
            }
1177
        }
1178
    }
1179
1180
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1181
    {
1182
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1183
        // Also make sure the documentation is up to date after.
1184
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1185
        // be better to have only one element IMO like:
1186
        //
1187
        // "datetime": {
1188
        //   "value": "val",
1189
        //   "format": "Y-m-d"
1190
        // }
1191
        //
1192
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1193
        // if the datetime format is the default one it's ok; but in any case the format should not
1194
        // be added without the datetime value...
1195
1196
        if (isset($raw->{'datetime'})) {
1197
            return $raw->{'datetime'};
1198
        }
1199
1200
        return null;
1201
    }
1202
1203
    private static function retrieveDatetimeNow(string $format)
1204
    {
1205
        $now = new DateTimeImmutable('now');
1206
1207
        $datetime = $now->format($format);
1208
1209
        if (!$datetime) {
1210
            throw new InvalidArgumentException(
1211
                sprintf(
1212
                    '""%s" is not a valid PHP date format',
1213
                    $format
1214
                )
1215
            );
1216
        }
1217
1218
        return $datetime;
1219
    }
1220
1221
    private static function retrieveDatetimeFormat(stdClass $raw): string
1222
    {
1223
        if (isset($raw->{'datetime_format'})) {
1224
            return $raw->{'datetime_format'};
1225
        }
1226
1227
        return self::DEFAULT_DATETIME_FORMAT;
1228
    }
1229
1230
    private static function retrieveReplacementSigil(stdClass $raw)
1231
    {
1232
        if (isset($raw->{'replacement-sigil'})) {
1233
            return $raw->{'replacement-sigil'};
1234
        }
1235
1236
        return self::DEFAULT_REPLACEMENT_SIGIL;
1237
    }
1238
1239
    private static function retrieveShebang(stdClass $raw): ?string
1240
    {
1241
        if (false === array_key_exists('shebang', (array) $raw)) {
1242
            return self::DEFAULT_SHEBANG;
1243
        }
1244
1245
        if (null === $raw->shebang) {
1246
            return null;
1247
        }
1248
1249
        $shebang = trim($raw->shebang);
1250
1251
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1252
        Assertion::true(
1253
            '#!' === substr($shebang, 0, 2),
1254
            sprintf(
1255
                'The shebang line must start with "#!". Got "%s" instead',
1256
                $shebang
1257
            )
1258
        );
1259
1260
        return $shebang;
1261
    }
1262
1263
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1264
    {
1265
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1266
        // TODO: trigger a warning if the signing algorithm used is weak
1267
        // TODO: no longer accept strings & document BC break
1268
        if (false === isset($raw->algorithm)) {
1269
            return Phar::SHA1;
1270
        }
1271
1272
        if (is_string($raw->algorithm)) {
1273
            if (false === defined('Phar::'.$raw->algorithm)) {
1274
                throw new InvalidArgumentException(
1275
                    sprintf(
1276
                        'The signing algorithm "%s" is not supported.',
1277
                        $raw->algorithm
1278
                    )
1279
                );
1280
            }
1281
1282
            return constant('Phar::'.$raw->algorithm);
1283
        }
1284
1285
        return $raw->algorithm;
1286
    }
1287
1288
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1289
    {
1290
        if (false === array_key_exists('banner', (array) $raw)) {
1291
            return self::DEFAULT_BANNER;
1292
        }
1293
1294
        if (null === $raw->banner) {
1295
            return null;
1296
        }
1297
1298
        $banner = $raw->banner;
1299
1300
        if (is_array($banner)) {
1301
            $banner = implode("\n", $banner);
1302
        }
1303
1304
        return $banner;
1305
    }
1306
1307
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1308
    {
1309
        if (false === isset($raw->{'banner-file'})) {
1310
            return null;
1311
        }
1312
1313
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1314
1315
        Assertion::file($bannerFile);
1316
1317
        return $bannerFile;
1318
    }
1319
1320
    private static function normalizeStubBannerContents(?string $contents): ?string
1321
    {
1322
        if (null === $contents) {
1323
            return null;
1324
        }
1325
1326
        $banner = explode("\n", $contents);
1327
        $banner = array_map('trim', $banner);
1328
1329
        return implode("\n", $banner);
1330
    }
1331
1332
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1333
    {
1334
        if (isset($raw->stub) && is_string($raw->stub)) {
1335
            $stubPath = make_path_absolute($raw->stub, $basePath);
1336
1337
            Assertion::file($stubPath);
1338
1339
            return $stubPath;
1340
        }
1341
1342
        return null;
1343
    }
1344
1345
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1346
    {
1347
        if (isset($raw->intercept)) {
1348
            return $raw->intercept;
1349
        }
1350
1351
        return false;
1352
    }
1353
1354
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1355
    {
1356
        if (isset($raw->{'key-pass'})
1357
            && (true === $raw->{'key-pass'})) {
1358
            return true;
1359
        }
1360
1361
        return false;
1362
    }
1363
1364
    private static function retrieveIsStubGenerated(stdClass $raw): bool
1365
    {
1366
        if (isset($raw->stub) && (true === $raw->stub)) {
1367
            return true;
1368
        }
1369
1370
        return false;
1371
    }
1372
1373
    /**
1374
     * Runs a Git command on the repository.
1375
     *
1376
     * @param string $command the command
1377
     *
1378
     * @return string the trimmed output from the command
1379
     */
1380
    private static function runGitCommand(string $command, string $file): string
1381
    {
1382
        $path = dirname($file);
1383
1384
        $process = new Process($command, $path);
1385
1386
        if (0 === $process->run()) {
1387
            return trim($process->getOutput());
1388
        }
1389
1390
        throw new RuntimeException(
1391
            sprintf(
1392
                'The tag or commit hash could not be retrieved from "%s": %s',
1393
                $path,
1394
                $process->getErrorOutput()
1395
            )
1396
        );
1397
    }
1398
1399
    private static function createPhpCompactor(stdClass $raw): Compactor
1400
    {
1401
        // TODO: false === not set; check & add test/doc
1402
        $tokenizer = new Tokenizer();
1403
1404
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1405
            $tokenizer->ignore(
1406
                (array) $raw->annotations->ignore
1407
            );
1408
        }
1409
1410
        return new Php($tokenizer);
1411
    }
1412
}
1413