Passed
Push — master ( 5dd2f3...e80b48 )
by Théo
02:17
created

Info::renderContents()   C

Complexity

Conditions 12
Paths 32

Size

Total Lines 65
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 65
rs 6.9666
c 0
b 0
f 0
cc 12
nc 32
nop 8

How to fix   Long Method    Complexity    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\Console\Command;
16
17
use function array_filter;
18
use function array_flip;
19
use function array_sum;
20
use Assert\Assertion;
21
use function count;
22
use DirectoryIterator;
23
use function end;
24
use function filesize;
25
use function is_array;
26
use function KevinGH\Box\FileSystem\remove;
27
use function KevinGH\Box\format_size;
28
use function KevinGH\Box\get_phar_compression_algorithms;
29
use KevinGH\Box\PharInfo\PharInfo;
30
use function key;
31
use Phar;
32
use PharData;
33
use PharFileInfo;
34
use function realpath;
35
use function round;
36
use function sprintf;
37
use function str_repeat;
38
use function str_replace;
39
use Symfony\Component\Console\Input\InputArgument;
40
use Symfony\Component\Console\Input\InputInterface;
41
use Symfony\Component\Console\Input\InputOption;
42
use Symfony\Component\Console\Output\OutputInterface;
43
use Symfony\Component\Console\Style\SymfonyStyle;
44
use Throwable;
45
46
/**
47
 * @private
48
 */
49
final class Info extends Command
50
{
51
    use CreateTemporaryPharFile;
52
53
    private const PHAR_ARG = 'phar';
54
    private const LIST_OPT = 'list';
55
    private const METADATA_OPT = 'metadata';
56
    private const MODE_OPT = 'mode';
57
    private const DEPTH_OPT = 'depth';
58
59
    private static $FILE_ALGORITHMS;
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function __construct(?string $name = null)
65
    {
66
        parent::__construct($name);
67
68
        if (null === self::$FILE_ALGORITHMS) {
69
            self::$FILE_ALGORITHMS = array_flip(array_filter(get_phar_compression_algorithms()));
70
        }
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected function configure(): void
77
    {
78
        $this->setName('info');
79
        $this->setDescription(
80
            '🔍  Displays information about the PHAR extension or file'
81
        );
82
        $this->setHelp(
83
            <<<'HELP'
84
The <info>%command.name%</info> command will display information about the Phar extension,
85
or the Phar file if specified.
86
87
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
88
about the PHAR file itself will be displayed.
89
90
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
91
be listed. By default, the list is shown as an indented tree. You may
92
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
93
to <comment>flat</comment>.
94
HELP
95
        );
96
        $this->addArgument(
97
            self::PHAR_ARG,
98
            InputArgument::OPTIONAL,
99
            'The Phar file.'
100
        );
101
        $this->addOption(
102
            self::LIST_OPT,
103
            'l',
104
            InputOption::VALUE_NONE,
105
            'List the contents of the Phar?'
106
        );
107
        $this->addOption(
108
            self::METADATA_OPT,
109
            null,
110
            InputOption::VALUE_NONE,
111
            'Display metadata?'
112
        );
113
        $this->addOption(
114
            self::MODE_OPT,
115
            'm',
116
            InputOption::VALUE_REQUIRED,
117
            'The listing mode. (default: indent, options: indent, flat)',
118
            'indent'
119
        );
120
        $this->addOption(
121
            self::DEPTH_OPT,
122
            'd',
123
            InputOption::VALUE_REQUIRED,
124
            'The depth of the tree displayed',
125
            -1
126
        );
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function execute(InputInterface $input, OutputInterface $output): int
133
    {
134
        $io = new SymfonyStyle($input, $output);
135
        $io->newLine();
136
137
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
138
            return $this->showGlobalInfo($output, $io);
139
        }
140
141
        $file = realpath($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type string[]; however, parameter $path of realpath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

141
        $file = realpath(/** @scrutinizer ignore-type */ $file);
Loading history...
142
143
        if (false === $file) {
144
            $io->error(
145
                sprintf(
146
                    'The file "%s" could not be found.',
147
                    $input->getArgument(self::PHAR_ARG)
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument(self::PHAR_ARG) can also be of type string[]; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

147
                    /** @scrutinizer ignore-type */ $input->getArgument(self::PHAR_ARG)
Loading history...
148
                )
149
            );
150
151
            return 1;
152
        }
153
154
        $tmpFile = $this->createTemporaryPhar($file);
155
156
        try {
157
            return $this->showInfo($tmpFile, $file, $input, $output, $io);
158
        } finally {
159
            if ($file !== $tmpFile) {
160
                remove($tmpFile);
161
            }
162
        }
163
    }
164
165
    public function showInfo(string $file, string $originalFile, InputInterface $input, OutputInterface $output, SymfonyStyle $io): int
166
    {
167
        $depth = (int) $input->getOption(self::DEPTH_OPT);
168
169
        Assertion::greaterOrEqualThan($depth, -1, 'Expected the depth to be a positive integer or -1, got "%d"');
170
171
        try {
172
            $pharInfo = new PharInfo($file);
173
174
            return $this->showPharInfo(
175
                $pharInfo,
176
                $input->getOption(self::LIST_OPT),
177
                $depth,
178
                'indent' === $input->getOption(self::MODE_OPT),
179
                $output,
180
                $io
181
            );
182
        } catch (Throwable $throwable) {
183
            if ($output->isDebug()) {
184
                throw $throwable;
185
            }
186
187
            $io->error(
188
                sprintf(
189
                    'Could not read the file "%s".',
190
                    $originalFile
191
                )
192
            );
193
194
            return 1;
195
        }
196
    }
197
198
    private function showGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
199
    {
200
        $this->render(
201
            $output,
202
            [
203
                'API Version' => Phar::apiVersion(),
204
                'Supported Compression' => Phar::getSupportedCompression(),
205
                'Supported Signatures' => Phar::getSupportedSignatures(),
206
            ]
207
        );
208
209
        $io->newLine();
210
        $io->comment('Get a PHAR details by giving its path as an argument.');
211
212
        return 0;
213
    }
214
215
    private function showPharInfo(
216
        PharInfo $pharInfo,
217
        bool $content,
218
        int $depth,
219
        bool $indent,
220
        OutputInterface $output,
221
        SymfonyStyle $io
222
    ): int {
223
        $signature = $pharInfo->getPhar()->getSignature();
224
225
        $this->showPharGlobalInfo($pharInfo, $io, $signature);
226
227
        if ($content) {
228
            $this->renderContents(
229
                $output,
230
                $pharInfo->getPhar(),
231
                0,
232
                $depth,
233
                $indent ? 0 : false,
234
                $pharInfo->getRoot(),
235
                $pharInfo->getPhar(),
236
                $pharInfo->getRoot()
237
            );
238
        } else {
239
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
240
        }
241
242
        return 0;
243
    }
244
245
    /**
246
     * @param mixed $signature
247
     */
248
    private function showPharGlobalInfo(PharInfo $pharInfo, SymfonyStyle $io, $signature): void
249
    {
250
        $io->writeln(
251
            sprintf(
252
                '<comment>API Version:</comment> %s',
253
                $pharInfo->getVersion()
254
            )
255
        );
256
        $io->newLine();
257
258
        $count = array_filter($pharInfo->retrieveCompressionCount());
259
        $totalCount = array_sum($count);
260
261
        if (1 === count($count)) {
262
            $io->writeln(
263
                sprintf(
264
                    '<comment>Archive Compression:</comment> %s',
265
                    key($count)
266
                )
267
            );
268
        } else {
269
            $io->writeln('<comment>Archive Compression:</comment>');
270
271
            end($count);
272
            $lastAlgorithmName = key($count);
273
274
            $totalPercentage = 100;
275
276
            foreach ($count as $algorithmName => $nbrOfFiles) {
277
                if ($lastAlgorithmName === $algorithmName) {
278
                    $percentage = $totalPercentage;
279
                } else {
280
                    $percentage = round($nbrOfFiles * 100 / $totalCount, 2);
281
282
                    $totalPercentage -= $percentage;
283
                }
284
285
                $io->writeln(
286
                    sprintf(
287
                        '  - %s (%0.2f%%)',
288
                        $algorithmName,
289
                        $percentage
290
                    )
291
                );
292
            }
293
        }
294
        $io->newLine();
295
296
        if (false !== $signature) {
297
            $io->writeln(
298
                sprintf(
299
                    '<comment>Signature:</comment> %s',
300
                    $signature['hash_type']
301
                )
302
            );
303
            $io->writeln(
304
                sprintf(
305
                    '<comment>Signature Hash:</comment> %s',
306
                    $signature['hash']
307
                )
308
            );
309
            $io->newLine();
310
        }
311
312
        $metadata = $pharInfo->getNormalizedMetadata();
313
314
        if (null === $metadata) {
315
            $io->writeln('<comment>Metadata:</comment> None');
316
        } else {
317
            $io->writeln('<comment>Metadata:</comment>');
318
            $io->writeln($metadata);
319
        }
320
        $io->newLine();
321
322
        $io->writeln(
323
            sprintf(
324
                '<comment>Contents:</comment>%s (%s)',
325
                1 === $totalCount ? ' 1 file' : " $totalCount files",
326
                format_size(
327
                    filesize($pharInfo->getPhar()->getPath())
328
                )
329
            )
330
        );
331
    }
332
333
    private function render(OutputInterface $output, array $attributes): void
334
    {
335
        $out = false;
336
337
        foreach ($attributes as $name => $value) {
338
            if ($out) {
339
                $output->writeln('');
340
            }
341
342
            $output->write("<comment>$name:</comment>");
343
344
            if (is_array($value)) {
345
                $output->writeln('');
346
347
                foreach ($value as $v) {
348
                    $output->writeln("  - $v");
349
                }
350
            } else {
351
                $output->writeln(" $value");
352
            }
353
354
            $out = true;
355
        }
356
    }
357
358
    /**
359
     * @param iterable|PharFileInfo[] $list
360
     * @param false|int               $indent Nbr of indent or `false`
361
     * @param Phar|PharData           $phar
362
     */
363
    private function renderContents(
364
        OutputInterface $output,
365
        iterable $list,
366
        int $depth,
367
        int $maxDepth,
368
        $indent,
369
        string $base,
370
        $phar,
371
        string $root
372
    ): void {
373
        if (-1 !== $maxDepth && $depth > $maxDepth) {
374
            return;
375
        }
376
377
        foreach ($list as $item) {
378
            $item = $phar[str_replace($root, '', $item->getPathname())];
379
380
            if (false !== $indent) {
381
                $output->write(str_repeat(' ', $indent));
382
383
                $path = $item->getFilename();
384
385
                if ($item->isDir()) {
386
                    $path .= '/';
387
                }
388
            } else {
389
                $path = str_replace($base, '', $item->getPathname());
390
            }
391
392
            if ($item->isDir()) {
393
                if (false !== $indent) {
394
                    $output->writeln("<info>$path</info>");
395
                }
396
            } else {
397
                $compression = '<fg=red>[NONE]</fg=red>';
398
399
                foreach (self::$FILE_ALGORITHMS as $code => $name) {
400
                    if ($item->isCompressed($code)) {
401
                        $compression = "<fg=cyan>[$name]</fg=cyan>";
402
                        break;
403
                    }
404
                }
405
406
                $fileSize = format_size($item->getCompressedSize());
407
408
                $output->writeln(
409
                    sprintf(
410
                        '%s %s - %s',
411
                        $path,
412
                        $compression,
413
                        $fileSize
414
                    )
415
                );
416
            }
417
418
            if ($item->isDir()) {
419
                $this->renderContents(
420
                    $output,
421
                    new DirectoryIterator($item->getPathname()),
422
                    $depth + 1,
423
                    $maxDepth,
424
                    false === $indent ? $indent : $indent + 2,
425
                    $base,
426
                    $phar,
427
                    $root
428
                );
429
            }
430
        }
431
    }
432
}
433