• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

move-elevator / composer-translation-validator / 16003193252

01 Jul 2025 03:09PM UTC coverage: 90.062% (-2.5%) from 92.521%
16003193252

Pull #13

github

web-flow
Merge 47aec355a into abafa7911
Pull Request #13: feat: introduce ResultType enum for improved validation result handling

13 of 26 new or added lines in 3 files covered. (50.0%)

12 existing lines in 1 file now uncovered.

435 of 483 relevant lines covered (90.06%)

2.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.83
/src/Command/ValidateTranslationCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace MoveElevator\ComposerTranslationValidator\Command;
6

7
use Composer\Command\BaseCommand;
8
use MoveElevator\ComposerTranslationValidator\FileDetector\Collector;
9
use MoveElevator\ComposerTranslationValidator\FileDetector\DetectorInterface;
10
use MoveElevator\ComposerTranslationValidator\Utility\ClassUtility;
11
use MoveElevator\ComposerTranslationValidator\Utility\PathUtility;
12
use MoveElevator\ComposerTranslationValidator\Validator\ResultType;
13
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorInterface;
14
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorRegistry;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Console\Command\Command;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputInterface;
19
use Symfony\Component\Console\Input\InputOption;
20
use Symfony\Component\Console\Logger\ConsoleLogger;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Console\Style\SymfonyStyle;
23

24
class ValidateTranslationCommand extends BaseCommand
25
{
26
    protected ?SymfonyStyle $io = null;
27
    protected ?InputInterface $input = null;
28
    protected ?OutputInterface $output = null;
29

30
    protected LoggerInterface $logger;
31

32
    protected ResultType $resultType = ResultType::SUCCESS;
33
    protected bool $dryRun = false;
34

35
    protected function configure(): void
10✔
36
    {
37
        $this->setName('validate-translations')
10✔
38
            ->setDescription('Validates translation files with several validators.')
10✔
39
            ->addArgument('path', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Paths to the folders containing XLIFF files')
10✔
40
            ->addOption('dry-run', 'dr', InputOption::VALUE_NONE, 'Run the command in dry-run mode without throwing errors')
10✔
41
            ->addOption('exclude', 'e', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patterns to exclude specific files')
10✔
42
            ->addOption('file-detector', 'fd', InputOption::VALUE_OPTIONAL, 'The file detector to use (FQCN)')
10✔
43
            ->addOption('validator', 'vd', InputOption::VALUE_OPTIONAL, 'The specific validator to use (FQCN)')
10✔
44
            ->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'Output format: cli or json', 'cli');
10✔
45
    }
46

47
    /**
48
     * @throws \ReflectionException|\JsonException
49
     */
50
    protected function execute(InputInterface $input, OutputInterface $output): int
9✔
51
    {
52
        $this->logger = new ConsoleLogger($output);
9✔
53

54
        $this->input = $input;
9✔
55
        $this->output = $output;
9✔
56
        $this->io = new SymfonyStyle($input, $output);
9✔
57

58
        $paths = array_map(static fn ($path) => str_starts_with((string) $path, '/') ? $path : getcwd().'/'.$path, $input->getArgument('path'));
9✔
59

60
        $this->dryRun = $input->getOption('dry-run');
9✔
61
        $excludePatterns = $input->getOption('exclude');
9✔
62

63
        $fileDetector = $this->validateAndInstantiate(
9✔
64
            DetectorInterface::class,
9✔
65
            'file detector',
9✔
66
            $input->getOption('file-detector')
9✔
67
        );
9✔
68

69
        if (empty($paths)) {
9✔
70
            $this->io->error('No paths provided.');
1✔
71

72
            return Command::FAILURE;
1✔
73
        }
74

75
        $allFiles = (new Collector($this->logger))->collectFiles($paths, $fileDetector, $excludePatterns);
8✔
76
        if (empty($allFiles)) {
8✔
77
            $this->io->warning('No files found in the specified directories.');
1✔
78

79
            return Command::SUCCESS;
1✔
80
        }
81

82
        if (!ClassUtility::validateClass(
7✔
83
            ValidatorInterface::class,
7✔
84
            $this->logger,
7✔
85
            $input->getOption('validator'))
7✔
86
        ) {
87
            $this->io->error(
1✔
88
                sprintf('The validator class "%s" must implement %s.',
1✔
89
                    $input->getOption('validator'),
1✔
90
                    ValidatorInterface::class
1✔
91
                )
1✔
92
            );
1✔
93

94
            return Command::FAILURE;
1✔
95
        }
96

97
        $this->validateAndInstantiate(
6✔
98
            ValidatorInterface::class,
6✔
99
            'validator',
6✔
100
            $input->getOption('validator')
6✔
101
        );
6✔
102

103
        $validators = $input->getOption('validator') ? [$input->getOption('validator')] : ValidatorRegistry::getAvailableValidators();
6✔
104
        $issues = [];
6✔
105

106
        // ToDo: Simplify this nested loop structure
107
        foreach ($allFiles as $parser => $paths) {
6✔
108
            foreach ($paths as $path => $translationSets) {
6✔
109
                foreach ($translationSets as $setKey => $files) {
6✔
110
                    foreach ($validators as $validator) {
6✔
111
                        $validatorInstance = new $validator($this->logger);
6✔
112
                        $result = $validatorInstance->validate($files, $parser);
6✔
113
                        if ($result) {
6✔
114
                            $this->resultType = $this->resultType->max($validatorInstance->resultTypeOnValidationFailure());
3✔
115
                            $issues[$validator][$path][$setKey] = $result;
3✔
116
                        }
117
                    }
118
                }
119
            }
120
        }
121

122
        $format = $input->getOption('format');
6✔
123

124
        return $this->summarize($issues, $format);
6✔
125
    }
126

127
    /**
128
     * Summarizes validation results in the specified format.
129
     *
130
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
131
     * @param string                                                                              $format Output format ('cli' or 'json')
132
     *
133
     * @return int Command exit code
134
     *
135
     * @throws \JsonException
136
     */
137
    private function summarize(array $issues, string $format = 'cli'): int
6✔
138
    {
139
        if ('json' === $format) {
6✔
140
            return $this->renderJsonResult($issues);
1✔
141
        }
142
        if ('cli' === $format) {
5✔
143
            return $this->renderCliResult($issues);
5✔
144
        }
UNCOV
145
        $this->io->error('Invalid output format specified. Use "cli" or "json".');
×
146

UNCOV
147
        return Command::FAILURE;
×
148
    }
149

150
    /**
151
     * Renders validation results as JSON output.
152
     *
153
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
154
     *
155
     * @return int Command exit code
156
     *
157
     * @throws \JsonException
158
     */
159
    private function renderJsonResult(array $issues): int
1✔
160
    {
161
        $result = [
1✔
162
            'status' => Command::SUCCESS,
1✔
163
            'message' => 'Language validation succeeded.',
1✔
164
            'issues' => $issues,
1✔
165
        ];
1✔
166

167
        if (!empty($issues)) {
1✔
UNCOV
168
            $result['message'] = 'Language validation failed.';
×
UNCOV
169
            $result['status'] = Command::FAILURE;
×
UNCOV
170
            if ($this->dryRun) {
×
UNCOV
171
                $result['message'] = 'Language validation failed and completed in dry-run mode.';
×
UNCOV
172
                $result['status'] = Command::SUCCESS;
×
173
            }
174
        }
175

176
        $this->output->writeln(json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
1✔
177

178
        return $result['status'];
1✔
179
    }
180

181
    /**
182
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
183
     */
184
    private function renderCliResult(array $issues): int
5✔
185
    {
186
        $this->renderIssues($issues);
5✔
187
        if ($this->resultType->notFullySuccessful()) {
5✔
188
            $this->io->newLine();
3✔
189
            $this->io->{$this->dryRun ? 'warning' : 'error'}(
3✔
190
                $this->dryRun
3✔
191
                    ? 'Language validation failed and completed in dry-run mode.'
1✔
192
                    : 'Language validation failed.'
3✔
193
            );
3✔
194
        } else {
195
            $message = 'Language validation succeeded.';
2✔
196
            $this->output->isVerbose()
2✔
NEW
UNCOV
197
                ? $this->io->success($message)
×
198
                : $this->output->writeln('<fg=green>'.$message.'</>');
2✔
199
        }
200

201
        return $this->resultType->resolveErrorToCommandExitCode($this->dryRun, false);
5✔
202
    }
203

204
    /**
205
     * Renders validation issues using validator-specific formatters.
206
     *
207
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
208
     */
209
    private function renderIssues(array $issues): void
5✔
210
    {
211
        foreach ($issues as $validator => $paths) {
5✔
212
            $validatorInstance = new $validator($this->logger);
3✔
213
            /* @var ValidatorInterface $validatorInstance */
214

215
            $this->io->section(sprintf('Validator: <fg=cyan>%s</>', $validator));
3✔
216
            foreach ($paths as $path => $sets) {
3✔
217
                if ($this->output->isVerbose()) {
3✔
218
                    $this->io->writeln(sprintf('Explanation: %s', $validatorInstance->explain()));
1✔
219
                }
220
                $this->io->writeln(sprintf('<fg=gray>Folder Path: %s</>', PathUtility::normalizeFolderPath($path)));
3✔
221
                $this->io->newLine();
3✔
222
                $validatorInstance->renderIssueSets(
3✔
223
                    $this->input,
3✔
224
                    $this->output,
3✔
225
                    $sets
3✔
226
                );
3✔
227

228
                $this->io->newLine();
3✔
229
                $this->io->newLine();
3✔
230
            }
231
        }
232
    }
233

234
    private function validateAndInstantiate(string $interface, string $type, ?string $className = null): ?object
9✔
235
    {
236
        if (null === $className) {
9✔
237
            return null;
9✔
238
        }
239

240
        if (!ClassUtility::validateClass($interface, $this->logger, $className)) {
1✔
UNCOV
241
            $this->io->error(
×
UNCOV
242
                sprintf('The %s class "%s" must implement %s.', $type, $className, $interface)
×
UNCOV
243
            );
×
244

UNCOV
245
            return null;
×
246
        }
247

248
        return new $className();
1✔
249
    }
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc