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

move-elevator / composer-translation-validator / 16119242009

07 Jul 2025 02:01PM UTC coverage: 94.526% (+1.3%) from 93.207%
16119242009

Pull #18

github

jackd248
feat: implement CLI and JSON renderers for validation results
Pull Request #18: refactor: implement object-oriented validation architecture with unified rendering system

228 of 230 new or added lines in 11 files covered. (99.13%)

7 existing lines in 2 files now uncovered.

777 of 822 relevant lines covered (94.53%)

5.23 hits per line

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

95.8
/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\Result\FormatType;
11
use MoveElevator\ComposerTranslationValidator\Result\Output;
12
use MoveElevator\ComposerTranslationValidator\Result\ValidationRun;
13
use MoveElevator\ComposerTranslationValidator\Utility\ClassUtility;
14
use MoveElevator\ComposerTranslationValidator\Validator\ResultType;
15
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorInterface;
16
use MoveElevator\ComposerTranslationValidator\Validator\ValidatorRegistry;
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\Console\Command\Command;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Logger\ConsoleLogger;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25

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

32
    protected LoggerInterface $logger;
33

34
    protected ResultType $resultType = ResultType::SUCCESS;
35
    protected bool $dryRun = false;
36
    protected bool $strict = false;
37

38
    protected function configure(): void
14✔
39
    {
40
        $this->setName('validate-translations')
14✔
41
            ->setDescription('Validates translation files with several validators.')
14✔
42
            ->addArgument(
14✔
43
                'path',
14✔
44
                InputArgument::IS_ARRAY | InputArgument::REQUIRED,
14✔
45
                'Paths to the folders containing translation files'
14✔
46
            )
14✔
47
            ->addOption(
14✔
48
                'dry-run',
14✔
49
                'dr',
14✔
50
                InputOption::VALUE_NONE,
14✔
51
                'Run the command in dry-run mode without throwing errors'
14✔
52
            )
14✔
53
            ->addOption(
14✔
54
                'strict',
14✔
55
                null,
14✔
56
                InputOption::VALUE_NONE,
14✔
57
                'Fail on warnings as errors'
14✔
58
            )
14✔
59
            ->addOption(
14✔
60
                'format',
14✔
61
                'f',
14✔
62
                InputOption::VALUE_OPTIONAL,
14✔
63
                'Output format: cli or json',
14✔
64
                FormatType::CLI->value
14✔
65
            )
14✔
66
            ->addOption(
14✔
67
                'exclude',
14✔
68
                'e',
14✔
69
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
14✔
70
                'Patterns to exclude specific files'
14✔
71
            )
14✔
72
            ->addOption(
14✔
73
                'file-detector',
14✔
74
                'fd',
14✔
75
                InputOption::VALUE_OPTIONAL,
14✔
76
                'The file detector to use (FQCN)'
14✔
77
            )
14✔
78
            ->addOption(
14✔
79
                'only',
14✔
80
                'o',
14✔
81
                InputOption::VALUE_OPTIONAL,
14✔
82
                'The specific validators to use (FQCN), comma-separated'
14✔
83
            )
14✔
84
            ->addOption(
14✔
85
                'skip',
14✔
86
                's',
14✔
87
                InputOption::VALUE_OPTIONAL,
14✔
88
                'Skip specific validators (FQCN), comma-separated'
14✔
89
            );
14✔
90
    }
91

92
    /**
93
     * @throws \ReflectionException|\JsonException
94
     */
95
    protected function execute(InputInterface $input, OutputInterface $output): int
13✔
96
    {
97
        $this->logger = new ConsoleLogger($output);
13✔
98

99
        $this->input = $input;
13✔
100
        $this->output = $output;
13✔
101
        $this->io = new SymfonyStyle($input, $output);
13✔
102

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

105
        $this->dryRun = $input->getOption('dry-run');
13✔
106
        $this->strict = $input->getOption('strict');
13✔
107
        $excludePatterns = $input->getOption('exclude');
13✔
108

109
        $fileDetector = ClassUtility::instantiate(
13✔
110
            DetectorInterface::class,
13✔
111
            $this->logger,
13✔
112
            'file detector',
13✔
113
            $input->getOption('file-detector')
13✔
114
        );
13✔
115

116
        if (empty($paths)) {
13✔
117
            $this->io->error('No paths provided.');
1✔
118

119
            return Command::FAILURE;
1✔
120
        }
121

122
        $allFiles = (new Collector($this->logger))->collectFiles($paths, $fileDetector, $excludePatterns);
12✔
123
        if (empty($allFiles)) {
12✔
124
            $this->io->warning('No files found in the specified directories.');
1✔
125

126
            return Command::SUCCESS;
1✔
127
        }
128

129
        $validators = $this->resolveValidators($input);
11✔
130
        $fileSets = ValidationRun::createFileSetsFromArray($allFiles);
11✔
131

132
        $validationRun = new ValidationRun($this->logger);
11✔
133
        $validationResult = $validationRun->executeFor($fileSets, $validators);
11✔
134

135

136
        $format = FormatType::tryFrom($input->getOption('format'));
10✔
137

138
        if (null === $format) {
10✔
139
            $this->io->error('Invalid output format specified. Use "cli" or "json".');
1✔
140

141
            return Command::FAILURE;
1✔
142
        }
143

144
        return (new Output(
9✔
145
            $this->logger,
9✔
146
            $this->output,
9✔
147
            $this->input,
9✔
148
            $format,
9✔
149
            $validationResult,
9✔
150
            $this->dryRun,
9✔
151
            $this->strict
9✔
152
        ))->summarize();
9✔
153
    }
154

155
    /**
156
     * @return array<int, class-string<ValidatorInterface>>
157
     */
158
    private function resolveValidators(InputInterface $input): array
11✔
159
    {
160
        $only = $this->validateClassInput(
11✔
161
            ValidatorInterface::class,
11✔
162
            'validator',
11✔
163
            $input->getOption('only')
11✔
164
        );
11✔
165
        $skip = $this->validateClassInput(
11✔
166
            ValidatorInterface::class,
11✔
167
            'validator',
11✔
168
            $input->getOption('skip')
11✔
169
        );
11✔
170

171
        if (!empty($only)) {
11✔
172
            $validators = $only;
1✔
173
        } elseif (!empty($skip)) {
10✔
UNCOV
174
            $validators = array_diff(ValidatorRegistry::getAvailableValidators(), $skip);
×
175
        } else {
176
            $validators = ValidatorRegistry::getAvailableValidators();
10✔
177
        }
178

179
        return $validators;
11✔
180
    }
181

182
    /**
183
     * @return array<int, class-string>
184
     */
185
    private function validateClassInput(string $interface, string $type, ?string $className = null): array
11✔
186
    {
187
        if (null === $className) {
11✔
188
            return [];
11✔
189
        }
190
        $classes = [];
1✔
191

192
        if (str_contains($className, ',')) {
1✔
193
            $classNames = explode(',', $className);
×
194
            foreach ($classNames as $name) {
×
195
                ClassUtility::instantiate($interface, $this->logger, $type, $name);
×
UNCOV
196
                $classes[] = $name;
×
197
            }
198
        } else {
199
            ClassUtility::instantiate($interface, $this->logger, $type, $className);
1✔
200
            $classes[] = $className;
1✔
201
        }
202

203
        return $classes;
1✔
204
    }
205
}
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

© 2026 Coveralls, Inc