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;
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 PhpMetrics binary name resolved through the runtime-aware tooling lookup
49     */
50    private const string BINARY = '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     * @var list<string> the directories PhpMetrics SHOULD skip by default
64     */
65    private const array DEFAULT_EXCLUDED_DIRECTORIES = [
66        'vendor',
67        'tmp',
68        'cache',
69        'spec',
70        'build',
71        ManagedWorkspace::WORKSPACE_ROOT,
72        'backup',
73        'resources',
74    ];
75
76    /**
77     * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process
78     * @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process
79     */
80    public function __construct(
81        private  ProcessBuilderInterface $processBuilder,
82        private  ProcessQueueInterface $processQueue,
83    ) {
84        parent::__construct();
85    }
86
87    /**
88     * @return void
89     */
90    protected function configure(): void
91    {
92        $this->setHelp('This command runs PhpMetrics to analyze the current working directory.');
93
94        $this->addJsonOption()
95            ->addOption(
96                name: 'progress',
97                mode: InputOption::VALUE_NONE,
98                description: 'Whether to enable progress output from PhpMetrics.',
99            )
100            ->addOption(
101                name: 'exclude',
102                mode: InputOption::VALUE_OPTIONAL,
103                description: 'Comma-separated directories that SHOULD be excluded from analysis.',
104                default: implode(',', $this->getDefaultExcludedDirectories()),
105            )
106            ->addOption(
107                name: 'target',
108                mode: InputOption::VALUE_OPTIONAL,
109                description: 'Target directory for the generated metrics reports.',
110                default: ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS),
111            )
112            ->addOption(
113                name: 'junit',
114                mode: InputOption::VALUE_OPTIONAL,
115                description: 'Optional target file for the generated JUnit XML report.',
116            );
117    }
118
119    /**
120     * @param InputInterface $input the runtime command input
121     * @param OutputInterface $output the console output stream
122     *
123     * @return int the command execution status code
124     */
125    protected function execute(InputInterface $input, OutputInterface $output): int
126    {
127        $jsonOutput = $this->isJsonOutput($input);
128        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
129        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
130
131        $target = rtrim((string) $input->getOption('target'), '/');
132        $exclude = (string) $input->getOption('exclude');
133        $junit = $input->getOption('junit');
134
135        $this->log('Running code metrics analysis...', $input);
136
137        $processBuilder = $this->processBuilder
138            ->withArgument('--ansi')
139            ->withArgument('--git', 'git')
140            ->withArgument('--exclude', $exclude)
141            ->withArgument('--report-html', $target)
142            ->withArgument('--report-json', $target . '/report.json')
143            ->withArgument('--report-summary-json', $target . '/report-summary.json');
144
145        if (! $progress) {
146            $processBuilder = $processBuilder->withArgument('--quiet');
147        }
148
149        if (null !== $junit) {
150            $processBuilder = $processBuilder->withArgument('--junit', $junit);
151        }
152
153        $this->processQueue->add(
154            process: $processBuilder
155                ->withArgument('.')
156                ->build([
157                    \PHP_BINARY,
158                    '-derror_reporting=' . self::PHP_ERROR_REPORTING,
159                    '-ddefault_socket_timeout=' . self::PHP_DEFAULT_SOCKET_TIMEOUT,
160                    DevToolsPathResolver::getPreferredToolBinaryPath(self::BINARY),
161                ]),
162            label: 'Generating Metrics with PhpMetrics',
163        );
164
165        $result = $this->processQueue->run($processOutput);
166
167        if (self::SUCCESS === $result) {
168            return $this->success('Code metrics analysis completed successfully.', $input, [
169                'output' => $processOutput,
170            ]);
171        }
172
173        return $this->failure('Code metrics analysis failed.', $input, [
174            'output' => $processOutput,
175        ]);
176    }
177
178    /**
179     * Returns the default PhpMetrics directory exclusion list.
180     *
181     * @return list<string>
182     */
183    private function getDefaultExcludedDirectories(): array
184    {
185        $directories = self::DEFAULT_EXCLUDED_DIRECTORIES;
186        $workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot();
187
188        if (null !== $workspaceRoot && ! \in_array($workspaceRoot, $directories, true)) {
189            $directories[] = $workspaceRoot;
190        }
191
192        return $directories;
193    }
194}