Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.25% covered (success)
98.25%
56 / 57
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeStyleCommand
98.25% covered (success)
98.25%
56 / 57
66.67% covered (warning)
66.67%
2 / 3
12
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%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.56% covered (success)
97.56%
40 / 41
0.00% covered (danger)
0.00%
0 / 1
10
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 Psr\Log\LoggerInterface;
27use Symfony\Component\Config\FileLocatorInterface;
28use Symfony\Component\Console\Attribute\AsCommand;
29use Symfony\Component\Console\Command\Command;
30use Symfony\Component\Console\Output\BufferedOutput;
31use Symfony\Component\Console\Input\InputInterface;
32use Symfony\Component\Console\Input\InputOption;
33use Symfony\Component\Console\Output\OutputInterface;
34
35/**
36 * Represents the command responsible for checking and fixing code style issues.
37 * This class MUST NOT be overridden and SHALL rely on external tools like ECS and Composer Normalize.
38 */
39#[AsCommand(
40    name: 'standards:code-style',
41    description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.',
42    aliases: ['code-style']
43)]
44 class CodeStyleCommand extends Command
45{
46    use HasJsonOption;
47    use LogsCommandResults;
48
49    /**
50     * @var string the default configuration file used for EasyCodingStandard
51     */
52    public const string CONFIG = 'ecs.php';
53
54    /**
55     * Constructs a new command instance responsible for orchestrating code style checks.
56     *
57     * The provided collaborators SHALL be used to locate the ECS configuration,
58     * build process definitions, and execute the resulting process queue. These
59     * dependencies MUST be valid service instances capable of supporting the
60     * command lifecycle expected by this class.
61     *
62     * @param FileLocatorInterface $fileLocator locates the configuration file required by EasyCodingStandard
63     * @param ProcessBuilderInterface $processBuilder builds the process instances used to execute Composer and ECS commands
64     * @param ProcessQueueInterface $processQueue queues and executes the generated processes in the required order
65     * @param LoggerInterface $logger logs command feedback
66     */
67    public function __construct(
68        private  FileLocatorInterface $fileLocator,
69        private  ProcessBuilderInterface $processBuilder,
70        private  ProcessQueueInterface $processQueue,
71        private  LoggerInterface $logger,
72    ) {
73        parent::__construct();
74    }
75
76    /**
77     * Configures the current command.
78     *
79     * This method MUST define the name, description, help text, and options for the command.
80     * It SHALL register the `--fix` option to allow automatic resolutions of style issues.
81     *
82     * @return void
83     */
84    protected function configure(): void
85    {
86        $this->setHelp(
87            'This command runs EasyCodingStandard and Composer Normalize to check and fix code style issues.'
88        );
89
90        $this->addJsonOption()
91            ->addOption(
92                name: 'progress',
93                mode: InputOption::VALUE_NONE,
94                description: 'Whether to enable progress output from code style tools.',
95            )
96            ->addOption(
97                name: 'fix',
98                shortcut: 'f',
99                mode: InputOption::VALUE_NONE,
100                description: 'Automatically fix code style issues.'
101            );
102    }
103
104    /**
105     * Executes the code style checks and fixes block.
106     *
107     * The method MUST execute `composer update --lock`, `composer normalize`, and ECS using secure processes.
108     * It SHALL return `self::SUCCESS` if all commands succeed, or `self::FAILURE` otherwise.
109     *
110     * @param InputInterface $input the input interface to retrieve options
111     * @param OutputInterface $output the output interface to log messages
112     *
113     * @return int the status code of the command
114     */
115    protected function execute(InputInterface $input, OutputInterface $output): int
116    {
117        $jsonOutput = $this->isJsonOutput($input);
118        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
119
120        $fix = (bool) $input->getOption('fix');
121        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
122
123        $this->logger->info('Running code style checks and fixes...');
124
125        $composerUpdate = $this->processBuilder
126            ->withArgument('--lock')
127            ->withArgument('--quiet')
128            ->build('composer update');
129
130        $composerNormalize = $this->processBuilder
131            ->withArgument('--ansi')
132            ->withArgument($fix ? '--quiet' : '--dry-run')
133            ->build('composer normalize');
134
135        $processBuilder = $this->processBuilder
136            ->withArgument('--ansi')
137            ->withArgument('--config', $this->fileLocator->locate(self::CONFIG));
138
139        if (! $progress) {
140            $processBuilder = $processBuilder->withArgument('--no-progress-bar');
141        }
142
143        if ($jsonOutput) {
144            $processBuilder = $processBuilder->withArgument('--output-format', 'json');
145        }
146
147        if ($fix) {
148            $processBuilder = $processBuilder->withArgument('--fix');
149        }
150
151        $ecs = $processBuilder->build('vendor/bin/ecs');
152
153        $this->processQueue->add(process: $composerUpdate, label: 'Refreshing Composer Lock');
154        $this->processQueue->add(
155            process: $composerNormalize,
156            label: 'Normalizing composer.json with Composer Normalize'
157        );
158        $this->processQueue->add(process: $ecs, label: 'Checking Code Style with Easy Coding Standard');
159
160        $result = $this->processQueue->run($processOutput);
161
162        if (self::SUCCESS === $result) {
163            return $this->success('Code style checks completed successfully.', $input, [
164                'fix' => $fix,
165                'config' => self::CONFIG,
166                'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null,
167            ]);
168        }
169
170        return $this->failure('Code style checks failed.', $input, [
171            'fix' => $fix,
172            'config' => self::CONFIG,
173            'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null,
174        ]);
175    }
176}