Passed
Pull Request — master (#88)
by Théo
02:23
created

Configuration   F

Complexity

Total Complexity 164

Size/Duplication

Total Lines 1369
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 164
dl 0
loc 1369
rs 0.6314
c 0
b 0
f 0

75 Methods

Rating   Name   Duplication   Size   Complexity  
A getFileMode() 0 3 1
A getMainScriptPath() 0 3 1
A getCompressionAlgorithm() 0 3 1
A getCompactors() 0 3 1
A getBinaryFiles() 0 3 1
A getAlias() 0 3 1
A getBasePath() 0 3 1
A getFiles() 0 3 1
A __construct() 0 60 1
A retrieveDatetimeFormat() 0 7 2
A getStubBannerContents() 0 3 1
A getFileMapper() 0 3 1
A retrieveMainScriptContents() 0 7 1
A retrieveMetadata() 0 15 3
A retrieveGitTag() 0 3 1
C retrieveProcessedReplacements() 0 39 8
A retrieveGitHashPlaceholder() 0 7 2
A retrieveDatetimeNow() 0 16 2
B retrieveCompactors() 0 27 5
A retrieveMainScriptPath() 0 5 2
B retrieveFiles() 0 35 3
A getTmpOutputPath() 0 3 1
A getShebang() 0 3 1
A retrieveStubBannerContents() 0 17 4
A createPhpCompactor() 0 12 3
A normalizePath() 0 3 1
A retrieveOutputPath() 0 15 3
A retrieveIsInterceptFileFuncs() 0 7 2
A retrieveFilesFromFinders() 0 12 2
A getStubBannerPath() 0 3 1
A retrieveBlacklistFilter() 0 10 2
A isInterceptFileFuncs() 0 3 1
A retrieveGitTagPlaceholder() 0 7 2
A retrieveAlias() 0 11 2
A retrieveReplacementSigil() 0 7 2
A retrieveGitHash() 0 8 2
A retrievePrivateKeyPassphrase() 0 10 3
A retrieveBasePath() 0 18 3
B retrieveMap() 0 25 5
B retrieveAllFiles() 0 34 3
A getStubPath() 0 3 1
A getProcessedReplacements() 0 3 1
A retrieveStubBannerPath() 0 11 2
A retrieveReplacements() 0 9 2
A retrievePhpScoperConfig() 0 21 3
A retrieveBlacklist() 0 13 2
A retrieveIsStubGenerated() 0 3 2
B retrieveSigningAlgorithm() 0 23 4
A retrieveShebang() 0 22 3
A isStubGenerated() 0 3 1
A getSigningAlgorithm() 0 3 1
A runGitCommand() 0 15 2
A retrieveIsPrivateKeyPrompt() 0 3 2
A getPrivateKeyPassphrase() 0 3 1
A normalizeStubBannerContents() 0 10 2
A shouldRetrieveAllFiles() 0 16 4
A retrieveGitVersionPlaceholder() 0 7 2
A retrievePrivateKeyPath() 0 10 2
D processFinder() 0 111 13
A retrieveDirectories() 0 18 2
A getMetadata() 0 3 1
A getOutputPath() 0 3 1
A processFinders() 0 11 1
A retrieveGitVersion() 0 19 3
B retrieveDirectoryPaths() 0 33 3
B retrieveCompressionAlgorithm() 0 41 4
A retrieveFileMode() 0 7 2
A getPrivateKeyPath() 0 3 1
A isPrivateKeyPrompt() 0 3 1
A retrieveGitShortHashPlaceholder() 0 7 2
A retrieveStubPath() 0 11 3
A retrieveDatetimeNowPlaceHolder() 0 21 2
A getMap() 0 3 1
B create() 0 93 3
A getMainScriptContents() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Configuration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Configuration, and based on these observations, apply Extract Interface, too.

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