Passed
Pull Request — master (#116)
by Théo
02:06
created

Configuration::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 67
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 9.2815
c 0
b 0
f 0
cc 1
eloc 34
nc 1
nop 28

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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