Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PhpDocCommand
98.28% covered (success)
98.28%
57 / 58
80.00% covered (warning)
80.00%
4 / 5
13
0.00% covered (danger)
0.00%
0 / 1
 configure
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 runPhpCsFixer
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 runRector
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 ensureDocHeaderExists
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
5.00
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/dev-tools.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/dev-tools
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\DevTools\Command;
20
21use Throwable;
22use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector;
23use Symfony\Component\Console\Input\InputInterface;
24use Symfony\Component\Console\Input\InputOption;
25use Symfony\Component\Console\Output\OutputInterface;
26use Symfony\Component\Process\Process;
27
28use function Safe\file_get_contents;
29
30/**
31 * Provides operations to inspect, lint, and repair PHPDoc comments across the project.
32 * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector.
33 */
34final class PhpDocCommand extends AbstractCommand
35{
36    /**
37     * @var string determines the template filename for docheaders
38     */
39    public const string FILENAME = '.docheader';
40
41    /**
42     * @var string stores the underlying configuration file for PHP-CS-Fixer
43     */
44    public const string CONFIG = '.php-cs-fixer.dist.php';
45
46    /**
47     * Configures the PHPDoc command.
48     *
49     * This method MUST securely configure the expected inputs, such as the `--fix` option.
50     *
51     * @return void
52     */
53    protected function configure(): void
54    {
55        $this
56            ->setName('phpdoc')
57            ->setDescription('Checks and fixes PHPDocs.')
58            ->setHelp('This command checks and fixes PHPDocs in your PHP files.')
59            ->addOption(
60                name: 'fix',
61                shortcut: 'f',
62                mode: InputOption::VALUE_NONE,
63                description: 'Whether to fix the PHPDoc issues automatically.',
64            );
65    }
66
67    /**
68     * Executes the PHPDoc checks and rectifications.
69     *
70     * The method MUST ensure the `.docheader` template is present. It SHALL then invoke
71     * PHP-CS-Fixer and Rector. If both succeed, it MUST return `self::SUCCESS`.
72     *
73     * @param InputInterface $input the command input parameters
74     * @param OutputInterface $output the system output handler
75     *
76     * @return int the success or failure state
77     */
78    protected function execute(InputInterface $input, OutputInterface $output): int
79    {
80        $output->writeln('<info>Checking and fixing PHPDocs...</info>');
81
82        $this->ensureDocHeaderExists($output);
83
84        $phpCsFixerResult = $this->runPhpCsFixer($input, $output);
85        $rectorResult = $this->runRector($input, $output);
86
87        return self::SUCCESS === $phpCsFixerResult && self::SUCCESS === $rectorResult ? self::SUCCESS : self::FAILURE;
88    }
89
90    /**
91     * Executes the PHP-CS-Fixer checks internally.
92     *
93     * The method SHOULD run in dry-run mode unless the fix flag is explicitly provided.
94     * It MUST return an integer describing the exit code.
95     *
96     * @param InputInterface $input the parsed console inputs
97     * @param OutputInterface $output the configured outputs
98     *
99     * @return int the status result of the underlying process
100     */
101    private function runPhpCsFixer(InputInterface $input, OutputInterface $output): int
102    {
103        $arguments = [
104            $this->getAbsolutePath('vendor/bin/php-cs-fixer'),
105            'fix',
106            '--config=' . parent::getConfigFile(self::CONFIG),
107            '--cache-file=' . $this->getCurrentWorkingDirectory() . '/tmp/cache/.php-cs-fixer.cache',
108            '--diff',
109        ];
110
111        if (! $input->getOption('fix')) {
112            $arguments[] = '--dry-run';
113        }
114
115        $command = new Process($arguments);
116
117        return parent::runProcess($command, $output);
118    }
119
120    /**
121     * Runs Rector to insert missing method block comments automatically.
122     *
123     * The method MUST apply the `AddMissingMethodPhpDocRector` constraint locally.
124     * It SHALL strictly return an integer denoting success or failure.
125     *
126     * @param InputInterface $input the incoming console parameters
127     * @param OutputInterface $output the outgoing console display
128     *
129     * @return int the code indicating the process result
130     */
131    private function runRector(InputInterface $input, OutputInterface $output): int
132    {
133        $arguments = [
134            $this->getAbsolutePath('vendor/bin/rector'),
135            'process',
136            '--config',
137            parent::getConfigFile(RefactorCommand::CONFIG),
138            '--autoload-file',
139            $this->getAbsolutePath('vendor/autoload.php'),
140            '--only',
141            '\\' . AddMissingMethodPhpDocRector::class,
142        ];
143
144        if (! $input->getOption('fix')) {
145            $arguments[] = '--dry-run';
146        }
147
148        $command = new Process($arguments);
149
150        return parent::runProcess($command, $output);
151    }
152
153    /**
154     * Creates the missing document header configuration file if needed.
155     *
156     * The method MUST query the local filesystem. If the file is missing, it SHOULD copy
157     * the tool template into the root folder.
158     *
159     * @param OutputInterface $output the logger where missing capabilities are announced
160     *
161     * @return void
162     */
163    private function ensureDocHeaderExists(OutputInterface $output): void
164    {
165        $projectDocHeader = self::getConfigFile(self::FILENAME, true);
166
167        if ($this->filesystem->exists($projectDocHeader)) {
168            return;
169        }
170
171        $repositoryDocHeader = self::getConfigFile(self::FILENAME);
172        $docHeader = file_get_contents($repositoryDocHeader);
173
174        try {
175            $composer = $this->requireComposer();
176            $rootPackageName = $composer->getPackage()
177                ->getName();
178
179            if ('' !== $rootPackageName) {
180                $docHeader = str_replace('fast-forward/dev-tools', $rootPackageName, $docHeader);
181            }
182        } catch (Throwable) {
183        }
184
185        try {
186            $this->filesystem->dumpFile($projectDocHeader, $docHeader);
187        } catch (Throwable) {
188            $output->writeln(
189                '<comment>Skipping .docheader creation because the destination file could not be written.</comment>'
190            );
191
192            return;
193        }
194
195        $output->writeln('<info>Created .docheader from repository template.</info>');
196    }
197}