Passed
Pull Request — master (#236)
by Théo
02:54
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
dl 0
loc 65
rs 5.9833
c 0
b 0
f 0
cc 12
eloc 37
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 Assert\Assertion;
18
use DateTimeImmutable;
19
use DirectoryIterator;
20
use Phar;
21
use PharData;
22
use PharFileInfo;
23
use RecursiveIteratorIterator;
24
use Symfony\Component\Console\Command\Command;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30
use Throwable;
31
use UnexpectedValueException;
32
use function array_fill_keys;
33
use function array_filter;
34
use function array_reduce;
35
use function array_sum;
36
use function count;
37
use function end;
38
use function filesize;
39
use function is_array;
40
use function iterator_to_array;
41
use function KevinGH\Box\FileSystem\copy;
42
use function KevinGH\Box\FileSystem\remove;
43
use function KevinGH\Box\format_size;
44
use function key;
45
use function realpath;
46
use function sprintf;
47
use function str_repeat;
48
use function str_replace;
49
use function sys_get_temp_dir;
50
use function var_export;
51
52
/**
53
 * @private
54
 */
55
final class Info extends Command
56
{
57
    private const PHAR_ARG = 'phar';
58
    private const LIST_OPT = 'list';
59
    private const METADATA_OPT = 'metadata';
60
    private const MODE_OPT = 'mode';
61
    private const DEPTH_OPT = 'depth';
62
63
    /**
64
     * The list of recognized compression algorithms.
65
     *
66
     * @var array
67
     */
68
    private const ALGORITHMS = [
69
        Phar::BZ2 => 'BZ2',
70
        Phar::GZ => 'GZ',
71
        Phar::NONE => 'None',
72
    ];
73
74
    /**
75
     * The list of recognized file compression algorithms.
76
     *
77
     * @var array
78
     */
79
    private const FILE_ALGORITHMS = [
80
        Phar::BZ2 => 'BZ2',
81
        Phar::GZ => 'GZ',
82
    ];
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    protected function configure(): void
88
    {
89
        $this->setName('info');
90
        $this->setDescription(
91
            'Displays information about the PHAR extension or file'
92
        );
93
        $this->setHelp(
94
            <<<'HELP'
95
The <info>%command.name%</info> command will display information about the Phar extension,
96
or the Phar file if specified.
97
98
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
99
about the PHAR file itself will be displayed.
100
101
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
102
be listed. By default, the list is shown as an indented tree. You may
103
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
104
to <comment>flat</comment>.
105
HELP
106
        );
107
        $this->addArgument(
108
            self::PHAR_ARG,
109
            InputArgument::OPTIONAL,
110
            'The Phar file.'
111
        );
112
        $this->addOption(
113
            self::LIST_OPT,
114
            'l',
115
            InputOption::VALUE_NONE,
116
            'List the contents of the Phar?'
117
        );
118
        $this->addOption(
119
            self::METADATA_OPT,
120
            null,
121
            InputOption::VALUE_NONE,
122
            'Display metadata?'
123
        );
124
        $this->addOption(
125
            self::MODE_OPT,
126
            'm',
127
            InputOption::VALUE_REQUIRED,
128
            'The listing mode. (default: indent, options: indent, flat)',
129
            'indent'
130
        );
131
        $this->addOption(
132
            self::DEPTH_OPT,
133
            'd',
134
            InputOption::VALUE_REQUIRED,
135
            'The depth of the tree displayed',
136
            -1
137
        );
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function execute(InputInterface $input, OutputInterface $output): int
144
    {
145
        $io = new SymfonyStyle($input, $output);
146
        $io->writeln('');
147
148
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
149
            return $this->showGlobalInfo($output, $io);
150
        }
151
152
        $file = realpath($file);
153
154
        if (false === $file) {
155
            $io->error(
156
                sprintf(
157
                    'The file "%s" could not be found.',
158
                    $input->getArgument(self::PHAR_ARG)
159
                )
160
            );
161
162
            return 1;
163
        }
164
165
        if ('' === pathinfo($file, PATHINFO_EXTENSION)) {
166
            // It is likely to be a PHAR without extension
167
            copy($file, $tmpFile = sys_get_temp_dir().'/'.(new DateTimeImmutable())->getTimestamp().$file.'.phar');
168
169
            try {
170
                return $this->showInfo($tmpFile, $file, $input, $output, $io);
171
            } finally {
172
                remove($tmpFile);
173
            }
174
        }
175
176
        return $this->showInfo($file, $file, $input, $output, $io);
177
    }
178
179
    public function showInfo(string $file, string $originalFile, InputInterface $input, OutputInterface $output, SymfonyStyle $io): int
180
    {
181
        $depth = (int) $input->getOption(self::DEPTH_OPT);
182
183
        Assertion::greaterOrEqualThan($depth, -1, 'Expected the depth to be a positive integer or -1, got "%d"');
184
185
        try {
186
            try {
187
                $phar = new Phar($file);
188
            } catch (UnexpectedValueException $exception) {
189
                $phar = new PharData($file);
190
            }
191
192
            return $this->showPharInfo(
193
                $phar,
194
                $input->getOption(self::LIST_OPT),
195
                $depth,
196
                'indent' === $input->getOption(self::MODE_OPT),
197
                $output,
198
                $io
199
            );
200
        } catch (Throwable $throwable) {
201
            if ($output->isDebug()) {
202
                throw $throwable;
203
            }
204
205
            $io->error(
206
                sprintf(
207
                    'Could not read the file "%s".',
208
                    $originalFile
209
                )
210
            );
211
212
            return 1;
213
        }
214
    }
215
216
    private function showGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
217
    {
218
        $this->render(
219
            $output,
220
            [
221
                'API Version' => Phar::apiVersion(),
222
                'Supported Compression' => Phar::getSupportedCompression(),
223
                'Supported Signatures' => Phar::getSupportedSignatures(),
224
            ]
225
        );
226
227
        $io->writeln('');
228
        $io->comment('Get a PHAR details by giving its path as an argument.');
229
230
        return 0;
231
    }
232
233
    /**
234
     * @param Phar|PharData $phar
235
     */
236
    private function showPharInfo(
237
        $phar,
238
        bool $content,
239
        int $depth,
240
        bool $indent,
241
        OutputInterface $output,
242
        SymfonyStyle $io
243
    ): int {
244
        $signature = $phar->getSignature();
245
246
        $this->showPharGlobalInfo($phar, $io, $signature);
247
248
        if ($content) {
249
            $root = 'phar://'.str_replace('\\', '/', realpath($phar->getPath())).'/';
250
251
            $this->renderContents(
252
                $output,
253
                $phar,
254
                0,
255
                $depth,
256
                $indent ? 0 : false,
257
                $root,
258
                $phar,
259
                $root
260
            );
261
        } else {
262
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
263
        }
264
265
        return 0;
266
    }
267
268
    /**
269
     * @param Phar|PharData $phar
270
     * @param mixed         $signature
271
     */
272
    private function showPharGlobalInfo($phar, SymfonyStyle $io, $signature): void
273
    {
274
        $io->writeln(
275
            sprintf(
276
                '<comment>API Version:</comment> %s',
277
                '' !== $phar->getVersion() ? $phar->getVersion() : 'No information found'
278
            )
279
        );
280
        $io->writeln('');
281
282
        $count = array_filter($this->retrieveCompressionCount($phar));
283
        $totalCount = array_sum($count);
284
285
        if (1 === count($count)) {
286
            $io->writeln(
287
                sprintf(
288
                    '<comment>Archive Compression:</comment> %s',
289
                    key($count)
290
                )
291
            );
292
        } else {
293
            $io->writeln('<comment>Archive Compression:</comment>');
294
295
            end($count);
296
            $lastAlgorithmName = key($count);
297
298
            $totalPercentage = 100;
299
300
            foreach ($count as $algorithmName => $nbrOfFiles) {
301
                if ($lastAlgorithmName === $algorithmName) {
302
                    $percentage = $totalPercentage;
303
                } else {
304
                    $percentage = $nbrOfFiles * 100 / $totalCount;
305
306
                    $totalPercentage -= $percentage;
307
                }
308
309
                $io->writeln(
310
                    sprintf(
311
                        '  - %s (%0.2f%%)',
312
                        $algorithmName,
313
                        $percentage
314
                    )
315
                );
316
            }
317
        }
318
        $io->writeln('');
319
320
        if (false !== $signature) {
321
            $io->writeln(
322
                sprintf(
323
                    '<comment>Signature:</comment> %s',
324
                    $signature['hash_type']
325
                )
326
            );
327
            $io->writeln(
328
                sprintf(
329
                    '<comment>Signature Hash:</comment> %s',
330
                    $signature['hash']
331
                )
332
            );
333
            $io->writeln('');
334
        }
335
336
        $metadata = var_export($phar->getMetadata(), true);
337
338
        if ('NULL' === $metadata) {
339
            $io->writeln('<comment>Metadata:</comment> None');
340
        } else {
341
            $io->writeln('<comment>Metadata:</comment>');
342
            $io->writeln($metadata);
343
        }
344
        $io->writeln('');
345
346
        $io->writeln(
347
            sprintf(
348
                '<comment>Contents:</comment>%s (%s)',
349
                1 === $totalCount ? ' 1 file' : " $totalCount files",
350
                format_size(
351
                    filesize($phar->getPath())
352
                )
353
            )
354
        );
355
    }
356
357
    private function render(OutputInterface $output, array $attributes): void
358
    {
359
        $out = false;
360
361
        foreach ($attributes as $name => $value) {
362
            if ($out) {
363
                $output->writeln('');
364
            }
365
366
            $output->write("<comment>$name:</comment>");
367
368
            if (is_array($value)) {
369
                $output->writeln('');
370
371
                foreach ($value as $v) {
372
                    $output->writeln("  - $v");
373
                }
374
            } else {
375
                $output->writeln(" $value");
376
            }
377
378
            $out = true;
379
        }
380
    }
381
382
    /**
383
     * @param OutputInterface         $output
384
     * @param iterable|PharFileInfo[] $list
385
     * @param false|int               $indent Nbr of indent or `false`
386
     * @param string                  $base
387
     * @param Phar|PharData           $phar
388
     * @param string                  $root
389
     */
390
    private function renderContents(
391
        OutputInterface $output,
392
        iterable $list,
393
        int $depth,
394
        int $maxDepth,
395
        $indent,
396
        string $base,
397
        $phar,
398
        string $root
399
    ): void {
400
        if (-1 !== $maxDepth && $depth > $maxDepth) {
401
            return;
402
        }
403
404
        foreach ($list as $item) {
405
            $item = $phar[str_replace($root, '', $item->getPathname())];
406
407
            if (false !== $indent) {
408
                $output->write(str_repeat(' ', $indent));
409
410
                $path = $item->getFilename();
411
412
                if ($item->isDir()) {
413
                    $path .= '/';
414
                }
415
            } else {
416
                $path = str_replace($base, '', $item->getPathname());
417
            }
418
419
            if ($item->isDir()) {
420
                if (false !== $indent) {
421
                    $output->writeln("<info>$path</info>");
422
                }
423
            } else {
424
                $compression = '<fg=red>[NONE]</fg=red>';
425
426
                foreach (self::FILE_ALGORITHMS as $code => $name) {
427
                    if ($item->isCompressed($code)) {
428
                        $compression = "<fg=cyan>[$name]</fg=cyan>";
429
                        break;
430
                    }
431
                }
432
433
                $fileSize = format_size($item->getCompressedSize());
434
435
                $output->writeln(
436
                    sprintf(
437
                        '%s %s - %s',
438
                        $path,
439
                        $compression,
440
                        $fileSize
441
                    )
442
                );
443
            }
444
445
            if ($item->isDir()) {
446
                $this->renderContents(
447
                    $output,
448
                    new DirectoryIterator($item->getPathname()),
449
                    $depth + 1,
450
                    $maxDepth,
451
                    (false === $indent) ? $indent : $indent + 2,
452
                    $base,
453
                    $phar,
454
                    $root
455
                );
456
            }
457
        }
458
    }
459
460
    /**
461
     * @param Phar|PharData $phar
462
     */
463
    private function retrieveCompressionCount($phar): array
464
    {
465
        $count = array_fill_keys(
466
           self::ALGORITHMS,
467
            0
468
        );
469
470
        if ($phar instanceof PharData) {
471
            $count[self::ALGORITHMS[$phar->isCompressed()]] = 1;
472
473
            return $count;
474
        }
475
476
        $countFile = function (array $count, PharFileInfo $file): array {
477
            if (false === $file->isCompressed()) {
478
                ++$count['None'];
479
480
                return $count;
481
            }
482
483
            foreach (self::ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) {
484
                if ($file->isCompressed($compressionAlgorithmCode)) {
485
                    ++$count[$compressionAlgorithmName];
486
487
                    return $count;
488
                }
489
            }
490
491
            return $count;
492
        };
493
494
        return array_reduce(
495
            iterator_to_array(new RecursiveIteratorIterator($phar)),
496
            $countFile,
497
            $count
498
        );
499
    }
500
}
501