Completed
Pull Request — master (#108)
by Théo
03:03
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 61
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

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