Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.00% covered (success)
98.00%
49 / 50
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CodeStyleCommand
98.00% covered (success)
98.00%
49 / 50
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%
12 / 12
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
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 Composer\Command\BaseCommand;
24use FastForward\DevTools\Console\Input\HasJsonOption;
25use FastForward\DevTools\Process\ProcessBuilderInterface;
26use FastForward\DevTools\Process\ProcessQueueInterface;
27use Psr\Log\LoggerInterface;
28use Symfony\Component\Config\FileLocatorInterface;
29use Symfony\Component\Console\Attribute\AsCommand;
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: 'code-style',
41    description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.',
42    help: 'This command runs EasyCodingStandard and Composer Normalize to check and fix code style issues.'
43)]
44 class CodeStyleCommand extends BaseCommand implements LoggerAwareCommandInterface
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->addJsonOption()
87            ->addOption(
88                name: 'progress',
89                mode: InputOption::VALUE_NONE,
90                description: 'Whether to enable progress output from code style tools.',
91            )
92            ->addOption(
93                name: 'fix',
94                shortcut: 'f',
95                mode: InputOption::VALUE_NONE,
96                description: 'Automatically fix code style issues.'
97            );
98    }
99
100    /**
101     * Executes the code style checks and fixes block.
102     *
103     * The method MUST execute `composer update --lock`, `composer normalize`, and ECS using secure processes.
104     * It SHALL return `self::SUCCESS` if all commands succeed, or `self::FAILURE` otherwise.
105     *
106     * @param InputInterface $input the input interface to retrieve options
107     * @param OutputInterface $output the output interface to log messages
108     *
109     * @return int the status code of the command
110     */
111    protected function execute(InputInterface $input, OutputInterface $output): int
112    {
113        $jsonOutput = $this->isJsonOutput($input);
114        $processOutput = $jsonOutput ? new BufferedOutput() : $output;
115
116        $fix = (bool) $input->getOption('fix');
117        $progress = ! $jsonOutput && (bool) $input->getOption('progress');
118
119        $this->logger->info('Running code style checks and fixes...');
120
121        $composerUpdate = $this->processBuilder
122            ->withArgument('--lock')
123            ->withArgument('--quiet')
124            ->build('composer update');
125
126        $composerNormalize = $this->processBuilder
127            ->withArgument('--ansi')
128            ->withArgument($fix ? '--quiet' : '--dry-run')
129            ->build('composer normalize');
130
131        $processBuilder = $this->processBuilder
132            ->withArgument('--config', $this->fileLocator->locate(self::CONFIG));
133
134        if (! $progress) {
135            $processBuilder = $processBuilder->withArgument('--no-progress-bar');
136        }
137
138        if ($jsonOutput) {
139            $processBuilder = $processBuilder->withArgument('--output-format', 'json');
140        }
141
142        if ($fix) {
143            $processBuilder = $processBuilder->withArgument('--fix');
144        }
145
146        $ecs = $processBuilder->build('vendor/bin/ecs');
147
148        $this->processQueue->add($composerUpdate);
149        $this->processQueue->add($composerNormalize);
150        $this->processQueue->add($ecs);
151
152        $result = $this->processQueue->run($processOutput);
153
154        if (self::SUCCESS === $result) {
155            return $this->success('Code style checks completed successfully.', $input, [
156                'fix' => $fix,
157                'config' => self::CONFIG,
158                'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null,
159            ]);
160        }
161
162        return $this->failure('Code style checks failed.', $input, [
163            'fix' => $fix,
164            'config' => self::CONFIG,
165            'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null,
166        ]);
167    }
168}