PharInfoRenderer   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 45
eloc 208
dl 0
loc 418
rs 8.8
c 2
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
B renderContent() 0 44 7
A extractBoxVersion() 0 9 3
A renderRequiredSection() 0 24 2
A renderParentDirectoriesIfNecessary() 0 38 4
A renderMetadata() 0 9 2
A renderCompression() 0 47 4
A renderBoxVersion() 0 15 2
A renderContentsSummary() 0 11 2
A renderShortSummary() 0 23 3
A renderVersion() 0 6 1
A renderSignature() 0 20 2
A renderConflictingSection() 0 17 2
A translateCompressionAlgorithm() 0 3 2
A renderRequirementChecker() 0 29 4
A retrieveRequirements() 0 28 2
A print() 0 11 2
A renderTimestamp() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like PharInfoRenderer 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 PharInfoRenderer, 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\Console;
16
17
use Closure;
18
use DateTimeImmutable;
19
use Fidry\Console\IO;
0 ignored issues
show
Bug introduced by
The type Fidry\Console\IO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use KevinGH\Box\Noop;
21
use KevinGH\Box\NotInstantiable;
22
use KevinGH\Box\Phar\CompressionAlgorithm;
23
use KevinGH\Box\Phar\PharInfo;
24
use KevinGH\Box\RequirementChecker\Requirement;
25
use KevinGH\Box\RequirementChecker\Requirements;
26
use KevinGH\Box\RequirementChecker\RequirementType;
27
use KevinGH\Box\RequirementChecker\Throwable\InvalidRequirements;
28
use KevinGH\Box\RequirementChecker\Throwable\NoRequirementsFound;
29
use SplFileInfo;
30
use Symfony\Component\Console\Output\OutputInterface;
31
use Symfony\Component\Filesystem\Path;
32
use function array_filter;
33
use function array_key_last;
34
use function array_map;
35
use function array_reduce;
36
use function array_sum;
37
use function array_values;
38
use function count;
39
use function implode;
40
use function iter\toArray;
41
use function KevinGH\Box\format_size;
42
use function key;
43
use function preg_match;
44
use function round;
45
use function Safe\filesize;
46
use function sprintf;
47
48
/**
49
 * Utility to write to the console output various PHAR related pieces of information.
50
 *
51
 * @private
52
 */
53
final class PharInfoRenderer
54
{
55
    use NotInstantiable;
56
57
    private const BOX_VERSION_PATTERN = '/ \* Generated by Humbug Box (?<version>.+)\.\s/';
58
    private const INDENT_SIZE = 2;
59
60
    public static function renderShortSummary(
61
        PharInfo $pharInfo,
62
        IO $io,
63
        ?Closure $separator = null,
64
    ): void {
65
        $separator ??= Noop::create();
66
67
        $methods = [
68
            self::renderCompression(...),
69
            self::renderSignature(...),
70
            self::renderMetadata(...),
71
            self::renderTimestamp(...),
72
            self::renderRequirementChecker(...),
73
            self::renderContentsSummary(...),
74
        ];
75
76
        $lastIndex = count($methods) - 1;
77
78
        foreach ($methods as $index => $method) {
79
            $method($pharInfo, $io);
80
81
            if ($index !== $lastIndex) {
82
                $separator();
83
            }
84
        }
85
    }
86
87
    public static function renderVersion(PharInfo $pharInfo, IO $io): void
88
    {
89
        $io->writeln(
90
            sprintf(
91
                '<comment>API Version:</comment> %s',
92
                $pharInfo->getVersion(),
93
            ),
94
        );
95
    }
96
97
    public static function renderBoxVersion(PharInfo $pharInfo, IO $io): void
98
    {
99
        $version = self::extractBoxVersion($pharInfo);
100
101
        if (null === $version) {
102
            return;
103
        }
104
105
        $io->writeln(
106
            sprintf(
107
                '<comment>Built with Box:</comment> %s',
108
                $version,
109
            ),
110
        );
111
        $io->newLine();
112
    }
113
114
    public static function renderCompression(PharInfo $pharInfo, IO $io): void
115
    {
116
        $io->writeln(
117
            sprintf(
118
                '<comment>Archive Compression:</comment> %s',
119
                self::translateCompressionAlgorithm($pharInfo->getCompression()),
120
            ),
121
        );
122
123
        $count = $pharInfo->getFilesCompressionCount();
124
        // Rename "none" to "None"
125
        $count['None'] = $count[CompressionAlgorithm::NONE->name];
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on KevinGH\Box\Phar\CompressionAlgorithm.
Loading history...
126
        unset($count[CompressionAlgorithm::NONE->name]);
127
        $count = array_filter($count);
128
129
        $totalCount = array_sum($count);
130
131
        if (1 === count($count)) {
132
            $io->writeln(
133
                sprintf(
134
                    '<comment>Files Compression:</comment> %s',
135
                    key($count),
136
                ),
137
            );
138
139
            return;
140
        }
141
142
        $io->writeln('<comment>Files Compression:</comment>');
143
        $lastAlgorithmName = array_key_last($count);
144
145
        $totalPercentage = 100;
146
147
        foreach ($count as $algorithmName => $nbrOfFiles) {
148
            if ($lastAlgorithmName === $algorithmName) {
149
                $percentage = $totalPercentage;
150
            } else {
151
                $percentage = round($nbrOfFiles * 100 / $totalCount, 2);
152
153
                $totalPercentage -= $percentage;
154
            }
155
156
            $io->writeln(
157
                sprintf(
158
                    '  - %s (%0.2f%%)',
159
                    $algorithmName,
160
                    $percentage,
161
                ),
162
            );
163
        }
164
    }
165
166
    public static function renderSignature(PharInfo $pharInfo, IO $io): void
167
    {
168
        $signature = $pharInfo->getSignature();
169
170
        if (null === $signature) {
171
            $io->writeln('<comment>Signature unreadable</comment>');
172
173
            return;
174
        }
175
176
        $io->writeln(
177
            sprintf(
178
                '<comment>Signature:</comment> %s',
179
                $signature['hash_type'],
180
            ),
181
        );
182
        $io->writeln(
183
            sprintf(
184
                '<comment>Signature Hash:</comment> %s',
185
                $signature['hash'],
186
            ),
187
        );
188
    }
189
190
    public static function renderMetadata(PharInfo $pharInfo, IO $io): void
191
    {
192
        $metadata = $pharInfo->getNormalizedMetadata();
193
194
        if (null === $metadata) {
195
            $io->writeln('<comment>Metadata:</comment> None');
196
        } else {
197
            $io->writeln('<comment>Metadata:</comment>');
198
            $io->writeln($metadata);
199
        }
200
    }
201
202
    public static function renderTimestamp(PharInfo $pharInfo, IO $io): void
203
    {
204
        $timestamp = $pharInfo->getTimestamp();
205
        $dateTime = (new DateTimeImmutable())->setTimestamp($timestamp);
206
207
        $io->writeln(
208
            sprintf(
209
                '<comment>Timestamp:</comment> %s (%s)',
210
                $timestamp,
211
                $dateTime->format(DateTimeImmutable::ATOM),
212
            ),
213
        );
214
    }
215
216
    public static function renderRequirementChecker(
217
        PharInfo $pharInfo,
218
        IO $io,
219
    ): void {
220
        try {
221
            $requirements = $pharInfo->getRequirements();
222
        } catch (NoRequirementsFound) {
223
            $io->writeln('<comment>RequirementChecker:</comment> Not found.');
224
225
            return;
226
        } catch (InvalidRequirements) {
227
            $io->writeln('<comment>RequirementChecker:</comment> Could not be checked.');
228
229
            return;
230
        }
231
232
        $io->write('<comment>RequirementChecker:</comment>');
233
234
        if (0 === count($requirements)) {
235
            $io->writeln(' No requirement found.');
236
237
            return;
238
        }
239
        $io->writeln('');
240
241
        [$required, $conflicting] = self::retrieveRequirements($requirements);
242
243
        self::renderRequiredSection($required, $io);
244
        self::renderConflictingSection($conflicting, $io);
245
    }
246
247
    public static function renderContentsSummary(PharInfo $pharInfo, IO $io): void
248
    {
249
        $count = array_filter($pharInfo->getFilesCompressionCount());
250
        $totalCount = array_sum($count);
251
252
        $io->writeln(
253
            sprintf(
254
                '<comment>Contents:</comment>%s (%s)',
255
                1 === $totalCount ? ' 1 file' : " {$totalCount} files",
256
                format_size(
257
                    filesize($pharInfo->getFile()),
258
                ),
259
            ),
260
        );
261
    }
262
263
    /**
264
     * @param false|positive-int|0 $maxDepth
0 ignored issues
show
Documentation Bug introduced by
The doc comment false|positive-int|0 at position 2 could not be parsed: Unknown type name 'positive-int' at position 2 in false|positive-int|0.
Loading history...
265
     * @param false|int            $indent   Nbr of indent or `false`
266
     */
267
    public static function renderContent(
268
        OutputInterface $output,
269
        PharInfo $pharInfo,
270
        false|int $maxDepth,
271
        bool $indent,
272
    ): void {
273
        $depth = 0;
274
        $renderedDirectories = [];
275
276
        foreach ($pharInfo->getFiles() as $splFileInfo) {
277
            if (false !== $maxDepth && $depth > $maxDepth) {
278
                continue;
279
            }
280
281
            if ($indent) {
282
                self::renderParentDirectoriesIfNecessary(
283
                    $splFileInfo,
284
                    $output,
285
                    $depth,
286
                    $renderedDirectories,
287
                );
288
            }
289
290
            [
291
                'compression' => $compression,
292
                'compressedSize' => $compressionSize,
293
            ] = $pharInfo->getFileMeta($splFileInfo->getRelativePathname());
294
295
            $compressionLine = CompressionAlgorithm::NONE === $compression
296
                ? '<fg=red>[NONE]</fg=red>'
297
                : "<fg=cyan>[{$compression->name}]</fg=cyan>";
298
299
            self::print(
300
                $output,
301
                sprintf(
302
                    '%s %s - %s',
303
                    $indent
304
                        ? $splFileInfo->getFilename()
305
                        : $splFileInfo->getRelativePathname(),
306
                    $compressionLine,
307
                    format_size($compressionSize),
308
                ),
309
                $depth,
310
                $indent,
311
            );
312
        }
313
    }
314
315
    private static function extractBoxVersion(PharInfo $pharInfo): ?string
316
    {
317
        $stub = $pharInfo->getStubContent();
318
319
        if (null !== $stub && 1 === preg_match(self::BOX_VERSION_PATTERN, $stub, $matches)) {
320
            return $matches['version'];
321
        }
322
323
        return null;
324
    }
325
326
    /**
327
     * @return array{Requirement[], Requirement[]}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{Requirement[], Requirement[]} at position 2 could not be parsed: Expected ':' at position 2, but found 'Requirement'.
Loading history...
328
     */
329
    private static function retrieveRequirements(Requirements $requirements): array
330
    {
331
        [$required, $conflicting] = array_reduce(
332
            toArray($requirements),
333
            static function ($carry, Requirement $requirement): array {
334
                $hash = implode(
335
                    ':',
336
                    [
337
                        $requirement->type->value,
338
                        $requirement->condition,
339
                        $requirement->source,
340
                    ],
341
                );
342
343
                if (RequirementType::EXTENSION_CONFLICT === $requirement->type) {
344
                    $carry[1][$hash] = $requirement;
345
                } else {
346
                    $carry[0][$hash] = $requirement;
347
                }
348
349
                return $carry;
350
            },
351
            [[], []],
352
        );
353
354
        return [
355
            array_values($required),
356
            array_values($conflicting),
357
        ];
358
    }
359
360
    /**
361
     * @param Requirement[] $required
362
     */
363
    private static function renderRequiredSection(
364
        array $required,
365
        IO $io,
366
    ): void {
367
        if (0 === count($required)) {
368
            return;
369
        }
370
371
        $io->writeln('  <comment>Required:</comment>');
372
        $io->writeln(
373
            array_map(
374
                static fn (Requirement $requirement) => match ($requirement->type) {
375
                    RequirementType::PHP => sprintf(
376
                        '  - PHP %s (%s)',
377
                        $requirement->condition,
378
                        $requirement->source ?? 'root',
379
                    ),
380
                    RequirementType::EXTENSION => sprintf(
381
                        '  - ext-%s (%s)',
382
                        $requirement->condition,
383
                        $requirement->source ?? 'root',
384
                    ),
385
                },
386
                $required,
387
            ),
388
        );
389
    }
390
391
    /**
392
     * @param Requirement[] $conflicting
393
     */
394
    private static function renderConflictingSection(
395
        array $conflicting,
396
        IO $io,
397
    ): void {
398
        if (0 === count($conflicting)) {
399
            return;
400
        }
401
402
        $io->writeln('  <comment>Conflict:</comment>');
403
        $io->writeln(
404
            array_map(
405
                static fn (Requirement $requirement) => sprintf(
406
                    '  - ext-%s (%s)',
407
                    $requirement->condition,
408
                    $requirement->source ?? 'root',
409
                ),
410
                $conflicting,
411
            ),
412
        );
413
    }
414
415
    private static function renderParentDirectoriesIfNecessary(
416
        SplFileInfo $fileInfo,
417
        OutputInterface $output,
418
        int &$depth,
419
        array &$renderedDirectories,
420
    ): void {
421
        $depth = 0;
422
        $relativePath = $fileInfo->getRelativePath();
0 ignored issues
show
Bug introduced by
The method getRelativePath() does not exist on SplFileInfo. Did you maybe mean getRealPath()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

422
        /** @scrutinizer ignore-call */ 
423
        $relativePath = $fileInfo->getRelativePath();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
423
424
        if ('' === $relativePath) {
425
            // No parent directory: there is nothing to do.
426
            return;
427
        }
428
429
        $parentDirectories = explode(
430
            '/',
431
            Path::normalize($relativePath),
432
        );
433
434
        foreach ($parentDirectories as $index => $parentDirectory) {
435
            if (array_key_exists($parentDirectory, $renderedDirectories)) {
436
                ++$depth;
437
438
                continue;
439
            }
440
441
            self::print(
442
                $output,
443
                "<info>{$parentDirectory}/</info>",
444
                $index,
445
                true,
446
            );
447
448
            $renderedDirectories[$parentDirectory] = true;
449
            ++$depth;
450
        }
451
452
        $depth = count($parentDirectories);
453
    }
454
455
    private static function print(
456
        OutputInterface $output,
457
        string $message,
458
        int $depth,
459
        bool $indent,
460
    ): void {
461
        if ($indent) {
462
            $output->write(str_repeat(' ', $depth * self::INDENT_SIZE));
463
        }
464
465
        $output->writeln($message);
466
    }
467
468
    private static function translateCompressionAlgorithm(CompressionAlgorithm $algorithm): string
469
    {
470
        return CompressionAlgorithm::NONE === $algorithm ? 'None' : $algorithm->name;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on KevinGH\Box\Phar\CompressionAlgorithm.
Loading history...
471
    }
472
}
473