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

move-elevator / composer-translation-validator / 16197314353

10 Jul 2025 02:04PM UTC coverage: 93.954% (-0.6%) from 94.544%
16197314353

Pull #23

github

jackd248
feat: add configuration documentation for translation validator and file detectors
Pull Request #23: feat: add configuration files and schema validation for translation validator

264 of 286 new or added lines in 5 files covered. (92.31%)

1181 of 1257 relevant lines covered (93.95%)

6.37 hits per line

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

85.45
/src/Config/ConfigReader.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace MoveElevator\ComposerTranslationValidator\Config;
6

7
use Symfony\Component\Yaml\Yaml;
8

9
class ConfigReader
10
{
11
    private const AUTO_DETECTION_FILES = [
12
        'translation-validator.php',
13
        'translation-validator.json',
14
        'translation-validator.yaml',
15
        'translation-validator.yml',
16
    ];
17

18
    /**
19
     * @throws \JsonException
20
     */
21
    public function read(string $configPath): TranslationValidatorConfig
26✔
22
    {
23
        if (!file_exists($configPath)) {
26✔
24
            throw new \InvalidArgumentException("Configuration file not found: {$configPath}");
1✔
25
        }
26

27
        if (!is_readable($configPath)) {
25✔
28
            throw new \RuntimeException("Configuration file is not readable: {$configPath}");
1✔
29
        }
30

31
        $extension = pathinfo($configPath, PATHINFO_EXTENSION);
24✔
32

33
        return match ($extension) {
24✔
34
            'php' => $this->readPhpConfig($configPath),
4✔
35
            'json' => $this->readJsonConfig($configPath),
15✔
36
            'yaml', 'yml' => $this->readYamlConfig($configPath),
4✔
37
            default => throw new \InvalidArgumentException("Unsupported configuration file format: {$extension}"),
12✔
38
        };
24✔
39
    }
40

41
    public function autoDetect(?string $workingDirectory = null): ?TranslationValidatorConfig
5✔
42
    {
43
        $workingDirectory ??= getcwd();
5✔
44

45
        if (!$workingDirectory) {
5✔
NEW
46
            return null;
×
47
        }
48

49
        foreach (self::AUTO_DETECTION_FILES as $filename) {
5✔
50
            $configPath = $workingDirectory.DIRECTORY_SEPARATOR.$filename;
5✔
51
            if (file_exists($configPath)) {
5✔
52
                return $this->read($configPath);
3✔
53
            }
54
        }
55

56
        return null;
2✔
57
    }
58

59
    /**
60
     * @throws \JsonException
61
     */
62
    public function readFromComposerJson(string $composerJsonPath): ?TranslationValidatorConfig
6✔
63
    {
64
        if (!file_exists($composerJsonPath)) {
6✔
65
            return null;
1✔
66
        }
67

68
        $composerData = json_decode(file_get_contents($composerJsonPath), true, 512, JSON_THROW_ON_ERROR);
5✔
69
        if (!is_array($composerData)) {
4✔
NEW
70
            return null;
×
71
        }
72

73
        $configFilePath = $composerData['extra']['translation-validator']['config-file'] ?? null;
4✔
74
        if (!$configFilePath) {
4✔
75
            return null;
1✔
76
        }
77

78
        $composerDir = dirname($composerJsonPath);
3✔
79
        $absoluteConfigPath = $this->resolveConfigPath($configFilePath, $composerDir);
3✔
80

81
        return $this->read($absoluteConfigPath);
3✔
82
    }
83

84
    private function readPhpConfig(string $configPath): TranslationValidatorConfig
4✔
85
    {
86
        $config = require $configPath;
4✔
87

88
        if (!$config instanceof TranslationValidatorConfig) {
4✔
89
            throw new \RuntimeException('PHP configuration file must return an instance of TranslationValidatorConfig');
1✔
90
        }
91

92
        return $config;
3✔
93
    }
94

95
    /**
96
     * @throws \JsonException
97
     */
98
    private function readJsonConfig(string $configPath): TranslationValidatorConfig
15✔
99
    {
100
        $content = file_get_contents($configPath);
15✔
101
        if (false === $content) {
15✔
NEW
102
            throw new \RuntimeException("Failed to read configuration file: {$configPath}");
×
103
        }
104

105
        $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
15✔
106
        if (!is_array($data)) {
14✔
NEW
107
            throw new \RuntimeException("Invalid JSON configuration file: {$configPath}");
×
108
        }
109

110
        return $this->createConfigFromArray($data);
14✔
111
    }
112

113
    /**
114
     * @throws \JsonException
115
     */
116
    private function readYamlConfig(string $configPath): TranslationValidatorConfig
4✔
117
    {
118
        $data = Yaml::parseFile($configPath);
4✔
119
        if (!is_array($data)) {
3✔
120
            throw new \RuntimeException("Invalid YAML configuration file: {$configPath}");
1✔
121
        }
122

123
        return $this->createConfigFromArray($data);
2✔
124
    }
125

126
    /**
127
     * @param array<string, mixed> $data
128
     *
129
     * @throws \JsonException
130
     */
131
    private function createConfigFromArray(array $data): TranslationValidatorConfig
16✔
132
    {
133
        // Validate against JSON schema if available
134
        $schemaValidator = new SchemaValidator();
16✔
135
        if ($schemaValidator->isAvailable()) {
16✔
136
            $schemaValidator->validate($data);
16✔
137
        }
138

139
        $config = new TranslationValidatorConfig();
8✔
140

141
        if (isset($data['paths'])) {
8✔
142
            if (!is_array($data['paths'])) {
8✔
NEW
143
                throw new \RuntimeException("Configuration 'paths' must be an array");
×
144
            }
145
            $config->setPaths($data['paths']);
8✔
146
        }
147

148
        if (isset($data['validators'])) {
8✔
149
            if (!is_array($data['validators'])) {
3✔
NEW
150
                throw new \RuntimeException("Configuration 'validators' must be an array");
×
151
            }
152
            $config->setValidators($data['validators']);
3✔
153
        }
154

155
        if (isset($data['file-detectors'])) {
8✔
156
            if (!is_array($data['file-detectors'])) {
3✔
NEW
157
                throw new \RuntimeException("Configuration 'file-detectors' must be an array");
×
158
            }
159
            $config->setFileDetectors($data['file-detectors']);
3✔
160
        }
161

162
        if (isset($data['parsers'])) {
8✔
163
            if (!is_array($data['parsers'])) {
3✔
NEW
164
                throw new \RuntimeException("Configuration 'parsers' must be an array");
×
165
            }
166
            $config->setParsers($data['parsers']);
3✔
167
        }
168

169
        if (isset($data['only'])) {
8✔
170
            if (!is_array($data['only'])) {
3✔
NEW
171
                throw new \RuntimeException("Configuration 'only' must be an array");
×
172
            }
173
            $config->setOnly($data['only']);
3✔
174
        }
175

176
        if (isset($data['skip'])) {
8✔
177
            if (!is_array($data['skip'])) {
3✔
NEW
178
                throw new \RuntimeException("Configuration 'skip' must be an array");
×
179
            }
180
            $config->setSkip($data['skip']);
3✔
181
        }
182

183
        if (isset($data['exclude'])) {
8✔
184
            if (!is_array($data['exclude'])) {
3✔
NEW
185
                throw new \RuntimeException("Configuration 'exclude' must be an array");
×
186
            }
187
            $config->setExclude($data['exclude']);
3✔
188
        }
189

190
        if (isset($data['strict'])) {
8✔
191
            if (!is_bool($data['strict'])) {
4✔
NEW
192
                throw new \RuntimeException("Configuration 'strict' must be a boolean");
×
193
            }
194
            $config->setStrict($data['strict']);
4✔
195
        }
196

197
        if (isset($data['dry-run'])) {
8✔
198
            if (!is_bool($data['dry-run'])) {
3✔
NEW
199
                throw new \RuntimeException("Configuration 'dry-run' must be a boolean");
×
200
            }
201
            $config->setDryRun($data['dry-run']);
3✔
202
        }
203

204
        if (isset($data['format'])) {
8✔
205
            if (!is_string($data['format'])) {
3✔
NEW
206
                throw new \RuntimeException("Configuration 'format' must be a string");
×
207
            }
208
            $this->validateFormat($data['format']);
3✔
209
            $config->setFormat($data['format']);
3✔
210
        }
211

212
        if (isset($data['verbose'])) {
8✔
213
            if (!is_bool($data['verbose'])) {
4✔
NEW
214
                throw new \RuntimeException("Configuration 'verbose' must be a boolean");
×
215
            }
216
            $config->setVerbose($data['verbose']);
4✔
217
        }
218

219
        return $config;
8✔
220
    }
221

222
    private function validateFormat(string $format): void
3✔
223
    {
224
        $allowedFormats = ['cli', 'json', 'yaml', 'php'];
3✔
225
        if (!in_array($format, $allowedFormats, true)) {
3✔
NEW
226
            throw new \RuntimeException("Invalid format '{$format}'. Allowed formats: ".implode(', ', $allowedFormats));
×
227
        }
228
    }
229

230
    private function resolveConfigPath(string $configPath, string $basePath): string
3✔
231
    {
232
        if (str_starts_with($configPath, '/') || preg_match('/^[a-zA-Z]:[\\/]/', $configPath)) {
3✔
233
            return $configPath;
1✔
234
        }
235

236
        return $basePath.DIRECTORY_SEPARATOR.$configPath;
2✔
237
    }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc