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

move-elevator / composer-translation-validator / 16002323940

01 Jul 2025 02:33PM UTC coverage: 92.521% (+2.9%) from 89.647%
16002323940

Pull #11

github

web-flow
Merge 91353df63 into da6f9b819
Pull Request #11: feat: add json output format

94 of 103 new or added lines in 6 files covered. (91.26%)

1 existing line in 1 file now uncovered.

433 of 468 relevant lines covered (92.52%)

2.89 hits per line

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

90.43
/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\ValidatorInterface;
13
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorRegistry;
14
use Psr\Log\LoggerInterface;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Logger\ConsoleLogger;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Style\SymfonyStyle;
22

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

29
    protected LoggerInterface $logger;
30

31
    protected bool $dryRun = false;
32

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

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

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

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

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

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

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

70
            return Command::FAILURE;
1✔
71
        }
72

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

77
            return Command::SUCCESS;
1✔
78
        }
79

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

92
            return Command::FAILURE;
1✔
93
        }
94

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

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

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

118
        $format = $input->getOption('format');
6✔
119

120
        return $this->summarize($issues, $format);
6✔
121
    }
122

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

NEW
143
        return Command::FAILURE;
×
144
    }
145

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

162
        if (!empty($issues)) {
1✔
NEW
163
            $result['message'] = 'Language validation failed.';
×
NEW
164
            $result['status'] = Command::FAILURE;
×
NEW
165
            if ($this->dryRun) {
×
NEW
166
                $result['message'] = 'Language validation failed and completed in dry-run mode.';
×
NEW
167
                $result['status'] = Command::SUCCESS;
×
168
            }
169
        }
170

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

173
        return $result['status'];
1✔
174
    }
175

176
    /**
177
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
178
     */
179
    private function renderCliResult(array $issues): int
5✔
180
    {
181
        $this->renderIssues($issues);
5✔
182
        if (!empty($issues)) {
5✔
183
            if ($this->dryRun) {
3✔
184
                $this->io->newLine();
1✔
185
                $this->io->warning('Language validation failed and completed in dry-run mode.');
1✔
186

187
                return Command::SUCCESS;
1✔
188
            }
189

190
            $this->io->newLine();
2✔
191
            $this->io->error('Language validation failed.');
2✔
192

193
            return Command::FAILURE;
2✔
194
        }
195

196
        $message = 'Language validation succeeded.';
2✔
197
        $this->output->isVerbose() ? $this->io->success($message) : $this->output->writeln('<fg=green>'.$message.'</>');
2✔
198

199
        return Command::SUCCESS;
2✔
200
    }
201

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

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

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

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

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

UNCOV
243
            return null;
×
244
        }
245

246
        return new $className();
1✔
247
    }
248
}
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