Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.51% covered (success)
98.51%
66 / 67
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetricsCommand
98.51% covered (success)
98.51%
66 / 67
75.00% covered (warning)
75.00%
3 / 4
11
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.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultExcludedDirectories
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
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\Path\DevToolsPathResolver;
25use FastForward\DevTools\Process\ProcessBuilderInterface;
26use FastForward\DevTools\Process\ProcessQueueInterface;
27use FastForward\DevTools\Path\ManagedWorkspace;
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;
34use function rtrim;
35
36#[AsCommand(
37    name: 'reports:metrics',
38    description: 'Analyzes code metrics with PhpMetrics.',
39    aliases: ['reports:phpmetrics', 'phpmetrics', 'metrics'],
40)]
41 class MetricsCommand extends Command
42{
43    use HasJsonOption;
44    use LogsCommandResults;
45
46    /**
47     * @var string the PhpMetrics binary name resolved through the runtime-aware tooling lookup
48     */
49    private const string BINARY = 'phpmetrics';
50
51    /**
52     * @var int the PHP error reporting mask that suppresses deprecations emitted by PhpMetrics internals
53     */
54    private const int PHP_ERROR_REPORTING = \E_ALL & ~\E_DEPRECATED;
55
56    /**
57     * @var int the maximum seconds PhpMetrics may wait on each Packagist package lookup
58     */
59    private const int PHP_DEFAULT_SOCKET_TIMEOUT = 1;
60
61    /**
62     * @var list<string> the directories PhpMetrics SHOULD skip by default
63     */
64    private const array DEFAULT_EXCLUDED_DIRECTORIES = [
65        'vendor',
66        'tmp',
67        'cache',
68        'spec',
69        'build',
70        ManagedWorkspace::WORKSPACE_ROOT,
71        'backup',
72        'resources',
73    ];
74
75    /**
76     * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process
77     * @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process
78     */
79    public function __construct(
80        private  ProcessBuilderInterface $processBuilder,
81        private  ProcessQueueInterface $processQueue,
82    ) {
83        parent::__construct();
84    }
85
86    /**
87     * @return void
88     */
89    protected function configure(): void
90    {
91        $this->setHelp('This command runs PhpMetrics to analyze the current working directory.');
92
93        $this->addJsonOption()
94            ->addOption(
95                name: 'progress',
96                mode: InputOption::VALUE_NONE,
97                description: 'Whether to enable progress output from PhpMetrics.',
98            )
99            ->addOption(
100                name: 'exclude',
101                mode: InputOption::VALUE_OPTIONAL,
102                description: 'Comma-separated directories that SHOULD be excluded from analysis.',
103                default: implode(',', $this->getDefaultExcludedDirectories()),
104            )
105            ->addOption(
106                name: 'target',
107                mode: InputOption::VALUE_OPTIONAL,
108                description: 'Target directory for the generated metrics reports.',
109                default: ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS),
110            )
111            ->addOption(
112                name: 'junit',
113                mode: InputOption::VALUE_OPTIONAL,
114                description: 'Optional target file for the generated JUnit XML report.',
115            );
116    }
117
118    /**
119     * @param InputInterface $input the runtime command input
120     * @param OutputInterface $output the console output stream
121     *
122     * @return int the command execution status code
123     */
124    protected function execute(InputInterface $input, OutputInterface $output): int
125    {
126        $jsonOutput = $this->isJsonOutput($input);
127        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
128        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
129
130        $target = rtrim((string) $input->getOption('target'), '/');
131        $exclude = (string) $input->getOption('exclude');
132        $junit = $input->getOption('junit');
133
134        $this->log('Running code metrics analysis...', $input);
135
136        $processBuilder = $this->processBuilder
137            ->withArgument('--ansi')
138            ->withArgument('--git', 'git')
139            ->withArgument('--exclude', $exclude)
140            ->withArgument('--report-html', $target)
141            ->withArgument('--report-json', $target . '/report.json')
142            ->withArgument('--report-summary-json', $target . '/report-summary.json');
143
144        if (! $progress) {
145            $processBuilder = $processBuilder->withArgument('--quiet');
146        }
147
148        if (null !== $junit) {
149            $processBuilder = $processBuilder->withArgument('--junit', $junit);
150        }
151
152        $this->processQueue->add(
153            process: $processBuilder
154                ->withArgument('.')
155                ->build([
156                    \PHP_BINARY,
157                    '-derror_reporting=' . self::PHP_ERROR_REPORTING,
158                    '-ddefault_socket_timeout=' . self::PHP_DEFAULT_SOCKET_TIMEOUT,
159                    DevToolsPathResolver::getPreferredToolBinaryPath(self::BINARY),
160                ]),
161            label: 'Generating Metrics with PhpMetrics',
162        );
163
164        $result = $this->processQueue->run($processOutput);
165
166        if (self::SUCCESS === $result) {
167            return $this->success('Code metrics analysis completed successfully.', $input, [
168                'output' => $processOutput,
169            ]);
170        }
171
172        return $this->failure('Code metrics analysis failed.', $input, [
173            'output' => $processOutput,
174        ]);
175    }
176
177    /**
178     * Returns the default PhpMetrics directory exclusion list.
179     *
180     * @return list<string>
181     */
182    private function getDefaultExcludedDirectories(): array
183    {
184        $directories = self::DEFAULT_EXCLUDED_DIRECTORIES;
185        $workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot();
186
187        if (null !== $workspaceRoot && ! \in_array($workspaceRoot, $directories, true)) {
188            $directories[] = $workspaceRoot;
189        }
190
191        return $directories;
192    }
193}