Passed
Pull Request — master (#74)
by Théo
02:17
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 60
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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