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

move-elevator / composer-translation-validator / 15991591801

01 Jul 2025 06:26AM UTC coverage: 85.714% (-3.9%) from 89.647%
15991591801

Pull #11

github

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

77 of 102 new or added lines in 6 files covered. (75.49%)

3 existing lines in 2 files now uncovered.

402 of 469 relevant lines covered (85.71%)

2.26 hits per line

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

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

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

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

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

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

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

67
        if (empty($paths)) {
6✔
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);
5✔
74
        if (empty($allFiles)) {
5✔
75
            $this->io->warning('No files found in the specified directories.');
1✔
76

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

80
        if (!ClassUtility::validateClass(
4✔
81
            ValidatorInterface::class,
4✔
82
            $this->logger,
4✔
83
            $input->getOption('validator'))
4✔
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(
3✔
96
            ValidatorInterface::class,
3✔
97
            'validator',
3✔
98
            $input->getOption('validator')
3✔
99
        );
3✔
100

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

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

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

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

123
    /**
124
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
125
     *
126
     * @throws \JsonException
127
     */
128
    private function summarize(array $issues, string $format = 'cli'): int
3✔
129
    {
130
        if ('json' === $format) {
3✔
NEW
131
            return $this->renderJsonResult($issues);
×
132
        }
133
        if ('cli' === $format) {
3✔
134
            return $this->renderCliResult($issues);
3✔
135
        }
NEW
136
        $this->io->error('Invalid output format specified. Use "cli" or "json".');
×
137

NEW
138
        return Command::FAILURE;
×
139
    }
140

141
    /**
142
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
143
     */
NEW
144
    private function renderJsonResult(array $issues): int
×
145
    {
NEW
146
        $result = [
×
NEW
147
            'status' => Command::SUCCESS,
×
NEW
148
            'message' => 'Language validation succeeded.',
×
NEW
149
            'issues' => $issues,
×
NEW
150
        ];
×
151

NEW
152
        if (!empty($issues)) {
×
NEW
153
            $result['message'] = 'Language validation failed.';
×
NEW
154
            $result['status'] = Command::FAILURE;
×
NEW
155
            if ($this->dryRun) {
×
NEW
156
                $result['message'] = 'Language validation failed and completed in dry-run mode.';
×
NEW
157
                $result['status'] = Command::SUCCESS;
×
158
            }
159
        }
160

NEW
161
        $this->output->writeln(json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
×
162

NEW
163
        return $result['status'];
×
164
    }
165

166
    /**
167
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
168
     */
169
    private function renderCliResult(array $issues): int
3✔
170
    {
171
        $this->renderIssues($issues);
3✔
172
        if (!empty($issues)) {
3✔
173
            if ($this->dryRun) {
2✔
174
                $this->io->newLine();
1✔
175
                $this->io->warning('Language validation failed and completed in dry-run mode.');
1✔
176

177
                return Command::SUCCESS;
1✔
178
            }
179

180
            $this->io->newLine();
1✔
181
            $this->io->error('Language validation failed.');
1✔
182

183
            return Command::FAILURE;
1✔
184
        }
185

186
        $message = 'Language validation succeeded.';
1✔
187
        $this->output->isVerbose() ? $this->io->success($message) : $this->output->writeln('<fg=green>'.$message.'</>');
1✔
188

189
        return Command::SUCCESS;
1✔
190
    }
191

192
    /**
193
     * @param array<class-string<ValidatorInterface>, array<string, array<string, array<mixed>>>> $issues
194
     */
195
    private function renderIssues(array $issues): void
3✔
196
    {
197
        foreach ($issues as $validator => $paths) {
3✔
198
            $validatorInstance = new $validator($this->logger);
2✔
199
            /* @var ValidatorInterface $validatorInstance */
200

201
            $this->io->section(sprintf('Validator: <fg=cyan>%s</>', $validator));
2✔
202
            foreach ($paths as $path => $sets) {
2✔
203
                if ($this->output->isVerbose()) {
2✔
NEW
204
                    $this->io->writeln(sprintf('Explanation: %s', $validatorInstance->explain()));
×
205
                }
206
                $this->io->writeln(sprintf('<fg=gray>Folder Path: %s</>', PathUtility::normalizeFolderPath($path)));
2✔
207
                $this->io->newLine();
2✔
208
                $validatorInstance->renderIssueSets(
2✔
209
                    $this->input,
2✔
210
                    $this->output,
2✔
211
                    $sets
2✔
212
                );
2✔
213

214
                $this->io->newLine();
2✔
215
                $this->io->newLine();
2✔
216
            }
217
        }
218
    }
219

220
    private function validateAndInstantiate(string $interface, string $type, ?string $className = null): ?object
6✔
221
    {
222
        if (null === $className) {
6✔
223
            return null;
6✔
224
        }
225

226
        if (!ClassUtility::validateClass($interface, $this->logger, $className)) {
×
227
            $this->io->error(
×
228
                sprintf('The %s class "%s" must implement %s.', $type, $className, $interface)
×
229
            );
×
230

231
            return null;
×
232
        }
233

UNCOV
234
        return new $className();
×
235
    }
236
}
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