Passed
Pull Request — master (#93)
by Théo
02:23
created

Info::retrieveCompressionCount()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 15
nc 1
nop 1
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 DirectoryIterator;
18
use function KevinGH\Box\formatted_filesize;
19
use Phar;
20
use PharFileInfo;
21
use RecursiveIteratorIterator;
22
use Symfony\Component\Console\Command\Command;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\OutputInterface;
27
use Symfony\Component\Console\Style\SymfonyStyle;
28
use function array_fill_keys;
29
use function array_filter;
30
use function array_reduce;
31
use function array_sum;
32
use function end;
33
use function is_array;
34
use function iterator_to_array;
35
use function key;
36
use function realpath;
37
use function sprintf;
38
39
final class Info extends Command
40
{
41
    private const PHAR_ARG = 'phar';
42
    private const LIST_OPT = 'list';
43
    private const METADATA_OPT = 'metadata';
44
    private const MODE_OPT = 'mode';
45
46
    /**
47
     * The list of recognized compression algorithms.
48
     *
49
     * @var array
50
     */
51
    private const ALGORITHMS = [
52
        Phar::BZ2 => 'BZ2',
53
        Phar::GZ => 'GZ',
54
        'NONE' => 'None',
55
    ];
56
57
    /**
58
     * The list of recognized file compression algorithms.
59
     *
60
     * @var array
61
     */
62
    private const FILE_ALGORITHMS = [
63
        Phar::BZ2 => 'BZ2',
64
        Phar::GZ => 'GZ',
65
    ];
66
67
    /**
68
     * @override
69
     */
70
    public function execute(InputInterface $input, OutputInterface $output): int
71
    {
72
        $io = new SymfonyStyle($input, $output);
73
        $io->writeln('');
74
75
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
76
            return $this->executeShowGlobalInfo($output, $io);
77
        }
78
79
        $phar = new Phar($file);
80
81
        return $this->executeShowPharInfo(
82
            $phar,
83
            $input->getOption(self::LIST_OPT),
84
            'indent' === $input->getOption(self::MODE_OPT),
85
            $output,
86
            $io
87
        );
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    protected function configure(): void
94
    {
95
        $this->setName('info');
96
        $this->setDescription(
97
            'Displays information about the PHAR extension or file'
98
        );
99
        $this->setHelp(
100
            <<<'HELP'
101
The <info>%command.name%</info> command will display information about the Phar extension,
102
or the Phar file if specified.
103
104
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
105
about the PHAR file itself will be displayed.
106
107
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
108
be listed. By default, the list is shown as an indented tree. You may
109
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
110
to <comment>flat</comment>.
111
HELP
112
        );
113
        $this->addArgument(
114
            self::PHAR_ARG,
115
            InputArgument::OPTIONAL,
116
            'The Phar file.'
117
        );
118
        $this->addOption(
119
            self::LIST_OPT,
120
            'l',
121
            InputOption::VALUE_NONE,
122
            'List the contents of the Phar?'
123
        );
124
        $this->addOption(
125
            self::METADATA_OPT,
126
            null,
127
            InputOption::VALUE_NONE,
128
            'Display metadata?'
129
        );
130
        $this->addOption(
131
            self::MODE_OPT,
132
            'm',
133
            InputOption::VALUE_OPTIONAL,
134
            'The listing mode. (default: indent, options: indent, flat)',
135
            'indent'
136
        );
137
    }
138
139
    private function executeShowGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
140
    {
141
        $this->render(
142
            $output,
143
            [
144
                'API Version' => Phar::apiVersion(),
145
                'Supported Compression' => Phar::getSupportedCompression(),
146
                'Supported Signatures' => Phar::getSupportedSignatures(),
147
            ]
148
        );
149
150
        $io->writeln('');
151
        $io->comment('Run the command with the PHAR path as an argument to get details on the PHAR.');
152
153
        return 0;
154
    }
155
156
    private function executeShowPharInfo(Phar $phar, bool $content, bool $indent, OutputInterface $output, SymfonyStyle $io): int
157
    {
158
        $signature = $phar->getSignature();
159
160
        $this->showPharGlobalInfo($phar, $io, $signature);
161
162
        if ($content) {
163
            $root = 'phar://'.str_replace('\\', '/', realpath($phar->getPath())).'/';
164
165
            $this->renderContents(
166
                $output,
167
                $phar,
0 ignored issues
show
Bug introduced by
$phar of type Phar is incompatible with the type iterable expected by parameter $list of KevinGH\Box\Console\Command\Info::renderContents(). ( Ignorable by Annotation )

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

167
                /** @scrutinizer ignore-type */ $phar,
Loading history...
168
                $indent ? 0 : false,
169
                $root,
170
                $phar,
171
                $root
172
            );
173
        } else {
174
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
175
        }
176
177
        return 0;
178
    }
179
180
    private function showPharGlobalInfo(Phar $phar, SymfonyStyle $io, $signature): void
181
    {
182
        $io->writeln(
183
            sprintf(
184
                '<comment>API Version:</comment> %s',
185
                $phar->getVersion()
186
            )
187
        );
188
        $io->writeln('');
189
190
        $count = array_filter($this->retrieveCompressionCount($phar));
191
        $totalCount = array_sum($count);
192
193
        if (1 === count($count)) {
194
            $io->writeln(
195
                sprintf(
196
                    '<comment>Archive Compression:</comment> %s',
197
                    key($count)
198
                )
199
            );
200
        } else {
201
            $io->writeln('<comment>Archive Compression:</comment>');
202
203
            end($count);
204
            $lastAlgorithmName = key($count);
205
206
            $totalPercentage = 100;
207
208
            foreach ($count as $algorithmName => $nbrOfFiles) {
209
                if ($lastAlgorithmName === $algorithmName) {
210
                    $percentage = $totalPercentage;
211
                } else {
212
                    $percentage = $nbrOfFiles * 100 / $totalCount;
213
214
                    $totalPercentage -= $percentage;
215
                }
216
217
                $io->writeln(
218
                    sprintf(
219
                        '  - %s (%0.2f%%)',
220
                        $algorithmName,
221
                        $percentage
222
                    )
223
                );
224
            }
225
        }
226
        $io->writeln('');
227
228
        $io->writeln(
229
            sprintf(
230
                '<comment>Signature:</comment> %s',
231
                $signature['hash_type']
232
            )
233
        );
234
        $io->writeln(
235
            sprintf(
236
                '<comment>Signature Hash:</comment> %s',
237
                $signature['hash']
238
            )
239
        );
240
        $io->writeln('');
241
242
        $metadata = var_export($phar->getMetadata(), true);
243
244
        if ('NULL' === $metadata) {
245
            $io->writeln('<comment>Metadata:</comment> None');
246
        } else {
247
            $io->writeln('<comment>Metadata:</comment>');
248
            $io->writeln($metadata);
249
        }
250
        $io->writeln('');
251
252
        $io->writeln(
253
            sprintf(
254
                '<comment>Contents:</comment>%s (%s)',
255
                1 === $totalCount ? ' 1 file' : " $totalCount files",
256
                formatted_filesize($phar->getPath())
257
            )
258
        );
259
    }
260
261
    private function render(OutputInterface $output, array $attributes): void
262
    {
263
        $out = false;
264
265
        foreach ($attributes as $name => $value) {
266
            if ($out) {
267
                $output->writeln('');
268
            }
269
270
            $output->write("<comment>$name:</comment>");
271
272
            if (is_array($value)) {
273
                $output->writeln('');
274
275
                foreach ($value as $v) {
276
                    $output->writeln("  - $v");
277
                }
278
            } else {
279
                $output->writeln(" $value");
280
            }
281
282
            $out = true;
283
        }
284
    }
285
286
    /**
287
     * @param OutputInterface         $output
288
     * @param iterable|PharFileInfo[] $list
289
     * @param $indent
290
     * @param string $base
291
     * @param Phar   $phar
292
     * @param string $root
293
     */
294
    private function renderContents(
295
        OutputInterface $output,
296
        iterable $list,
297
        $indent,
298
        string $base,
299
        Phar $phar,
300
        string $root
301
    ): void {
302
        foreach ($list as $item) {
303
            $item = $phar[str_replace($root, '', $item->getPathname())];
304
305
            if (false !== $indent) {
306
                $output->write(str_repeat(' ', $indent));
307
308
                $path = $item->getFilename();
0 ignored issues
show
Bug introduced by
The method getFilename() does not exist on integer. ( Ignorable by Annotation )

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

308
                /** @scrutinizer ignore-call */ 
309
                $path = $item->getFilename();

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...
309
310
                if ($item->isDir()) {
311
                    $path .= '/';
312
                }
313
            } else {
314
                $path = str_replace($base, '', $item->getPathname());
315
            }
316
317
            if ($item->isDir()) {
318
                if (false !== $indent) {
319
                    $output->writeln("<info>$path</info>");
320
                }
321
            } else {
322
                $compression = ' <fg=red>[NONE]</fg=red>';
323
324
                foreach (self::FILE_ALGORITHMS as $code => $name) {
325
                    if ($item->isCompressed($code)) {
326
                        $compression = " <fg=cyan>[$name]</fg=cyan>";
327
                        break;
328
                    }
329
                }
330
331
                $output->writeln($path.$compression);
332
            }
333
334
            if ($item->isDir()) {
335
                $this->renderContents(
336
                    $output,
337
                    new DirectoryIterator($item->getPathname()),
0 ignored issues
show
Bug introduced by
new DirectoryIterator($item->getPathname()) of type DirectoryIterator is incompatible with the type iterable expected by parameter $list of KevinGH\Box\Console\Command\Info::renderContents(). ( Ignorable by Annotation )

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

337
                    /** @scrutinizer ignore-type */ new DirectoryIterator($item->getPathname()),
Loading history...
338
                    (false === $indent) ? $indent : $indent + 2,
339
                    $base,
340
                    $phar,
341
                    $root
342
                );
343
            }
344
        }
345
    }
346
347
    private function retrieveCompressionCount(Phar $phar): array
348
    {
349
        $count = array_fill_keys(
350
           self::ALGORITHMS,
351
            0
352
        );
353
354
        $countFile = function (array $count, PharFileInfo $file) {
355
            if (false === $file->isCompressed()) {
356
                ++$count['None'];
357
358
                return $count;
359
            }
360
361
            foreach (self::ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) {
362
                if ($file->isCompressed($compressionAlgorithmCode)) {
363
                    ++$count[$compressionAlgorithmName];
364
365
                    return $count;
366
                }
367
            }
368
369
            return $count;
370
        };
371
372
        return array_reduce(
373
            iterator_to_array(new RecursiveIteratorIterator($phar)),
374
            $countFile,
375
            $count
376
        );
377
    }
378
}
379