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

Configuration   F

Complexity

Total Complexity 170

Size/Duplication

Total Lines 1400
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 170
dl 0
loc 1400
rs 0.6314
c 0
b 0
f 0

77 Methods

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