Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.44% covered (success)
98.44%
63 / 64
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetricsCommand
98.44% covered (success)
98.44%
63 / 64
66.67% covered (warning)
66.67%
2 / 3
8
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\Command;
21
22use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
23use FastForward\DevTools\Console\Input\HasJsonOption;
24use FastForward\DevTools\Process\ProcessBuilderInterface;
25use FastForward\DevTools\Process\ProcessQueueInterface;
26use FastForward\DevTools\Path\ManagedWorkspace;
27use Psr\Log\LoggerInterface;
28use Symfony\Component\Console\Attribute\AsCommand;
29use Symfony\Component\Console\Command\Command;
30use Symfony\Component\Console\Input\InputInterface;
31use Symfony\Component\Console\Input\InputOption;
32use Symfony\Component\Console\Output\BufferedOutput;
33use Symfony\Component\Console\Output\OutputInterface;
34
35use function rtrim;
36
37#[AsCommand(
38    name: 'reports:metrics',
39    description: 'Analyzes code metrics with PhpMetrics.',
40    aliases: ['reports:phpmetrics', 'phpmetrics', 'metrics'],
41)]
42 class MetricsCommand extends Command
43{
44    use HasJsonOption;
45    use LogsCommandResults;
46
47    /**
48     * @var string the bundled PhpMetrics binary path relative to the consumer root
49     */
50    private const string BINARY = 'vendor/bin/phpmetrics';
51
52    /**
53     * @var int the PHP error reporting mask that suppresses deprecations emitted by PhpMetrics internals
54     */
55    private const int PHP_ERROR_REPORTING = \E_ALL & ~\E_DEPRECATED;
56
57    /**
58     * @var int the maximum seconds PhpMetrics may wait on each Packagist package lookup
59     */
60    private const int PHP_DEFAULT_SOCKET_TIMEOUT = 1;
61
62    /**
63     * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process
64     * @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process
65     * @param LoggerInterface $logger the output-aware logger
66     */
67    public function __construct(
68        private  ProcessBuilderInterface $processBuilder,
69        private  ProcessQueueInterface $processQueue,
70        private  LoggerInterface $logger,
71    ) {
72        parent::__construct();
73    }
74
75    /**
76     * @return void
77     */
78    protected function configure(): void
79    {
80        $this->setHelp('This command runs PhpMetrics to analyze the current working directory.');
81
82        $this->addJsonOption()
83            ->addOption(
84                name: 'progress',
85                mode: InputOption::VALUE_NONE,
86                description: 'Whether to enable progress output from PhpMetrics.',
87            )
88            ->addOption(
89                name: 'exclude',
90                mode: InputOption::VALUE_OPTIONAL,
91                description: 'Comma-separated directories that SHOULD be excluded from analysis.',
92                default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures',
93            )
94            ->addOption(
95                name: 'target',
96                mode: InputOption::VALUE_OPTIONAL,
97                description: 'Target directory for the generated metrics reports.',
98                default: ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS),
99            )
100            ->addOption(
101                name: 'junit',
102                mode: InputOption::VALUE_OPTIONAL,
103                description: 'Optional target file for the generated JUnit XML report.',
104            );
105    }
106
107    /**
108     * @param InputInterface $input the runtime command input
109     * @param OutputInterface $output the console output stream
110     *
111     * @return int the command execution status code
112     */
113    protected function execute(InputInterface $input, OutputInterface $output): int
114    {
115        $jsonOutput = $this->isJsonOutput($input);
116        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
117        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
118
119        $target = rtrim((string) $input->getOption('target'), '/');
120        $exclude = (string) $input->getOption('exclude');
121        $junit = $input->getOption('junit');
122
123        $this->logger->info('Running code metrics analysis...', [
124            'input' => $input,
125        ]);
126
127        $processBuilder = $this->processBuilder
128            ->withArgument('--ansi')
129            ->withArgument('--git', 'git')
130            ->withArgument('--exclude', $exclude)
131            ->withArgument('--report-html', $target)
132            ->withArgument('--report-json', $target . '/report.json')
133            ->withArgument('--report-summary-json', $target . '/report-summary.json');
134
135        if (! $progress) {
136            $processBuilder = $processBuilder->withArgument('--quiet');
137        }
138
139        if (null !== $junit) {
140            $processBuilder = $processBuilder->withArgument('--junit', $junit);
141        }
142
143        $this->processQueue->add(
144            process: $processBuilder
145                ->withArgument('.')
146                ->build([
147                    \PHP_BINARY,
148                    '-derror_reporting=' . self::PHP_ERROR_REPORTING,
149                    '-ddefault_socket_timeout=' . self::PHP_DEFAULT_SOCKET_TIMEOUT,
150                    self::BINARY,
151                ]),
152            label: 'Generating Metrics with PhpMetrics',
153        );
154
155        $result = $this->processQueue->run($processOutput);
156
157        if (self::SUCCESS === $result) {
158            return $this->success('Code metrics analysis completed successfully.', $input, [
159                'output' => $processOutput,
160            ]);
161        }
162
163        return $this->failure('Code metrics analysis failed.', $input, [
164            'output' => $processOutput,
165        ]);
166    }
167}