Passed
Push — main ( 42d171...ad805a )
by Dimitri
05:59 queued 01:56
created

TranslationsFinder::process()   F

Complexity

Conditions 19
Paths 304

Size

Total Lines 96
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 19.2475

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 56
c 2
b 0
f 0
nc 304
nop 2
dl 0
loc 96
ccs 31
cts 34
cp 0.9118
crap 19.2475
rs 2.3833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Cli\Commands\Translation;
13
14
use Ahc\Cli\Output\Color;
15
use BlitzPHP\Cli\Console\Command;
16
use BlitzPHP\Utilities\Iterable\Arr;
17
use Locale;
18
use RecursiveDirectoryIterator;
19
use RecursiveIteratorIterator;
20
use SplFileInfo;
21
22
/**
23
 * @credit <a href="https://codeigniter.com">CodeIgniter 4 - \CodeIgniter\Commands\Translation\LocalizationFinder</a>
24
 */
25
class TranslationsFinder extends Command
26
{
27
    protected $group       = 'Translation';
28
    protected $name        = 'translations:find';
29
    protected $description = 'Trouver et sauvegarder les phrases disponibles à traduire';
30
    protected $options     = [
31
        '--locale'   => 'Spécifier la locale (en, ru, etc.) pour enregistrer les fichiers',
32
        '--dir'      => 'Répertoire de recherche des traductions relatif à APP_PATH.',
33
        '--show-new' => 'N\'affiche que les nouvelles traductions dans le tableau. N\'écrit pas dans les fichiers.',
34
        '--verbose'  => 'Affiche des informations détaillées',
35
    ];
36
37
    /**
38
     * Indicateur pour afficher des informations détaillées
39
     */
40
    private bool $verbose = false;
41
42
    /**
43
     * Indicateur pour afficher uniquement les traductions, sans sauvegarde
44
     */
45
    private bool $showNew = false;
46
47
    private string $languagePath;
48
49
    /**
50
     * {@inheritDoc}
51
     */
52
    public function execute(array $params)
53
    {
54 2
        $this->verbose      = $this->option('verbose', false);
55 2
        $this->showNew      = $this->option('show-new', false);
56 2
        $optionLocale       = $params['locale'] ?? null;
57 2
        $optionDir          = $params['dir'] ?? null;
58 2
        $currentLocale      = Locale::getDefault();
59 2
        $currentDir         = APP_PATH;
60 2
        $this->languagePath = $currentDir . 'Translations';
61
62
        if (on_test()) {
63 2
            $currentDir         = ROOTPATH . 'Services' . DS;
64 2
            $this->languagePath = ROOTPATH . 'Translations';
65
        }
66
67
        if (is_string($optionLocale)) {
68
            if (! in_array($optionLocale, config('app.supported_locales'), true)) {
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
            if (! in_array($optionLocale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
69
                $this->error(
70
                    $this->color->error('"' . $optionLocale . '" n\'est pas supporté. Les langues supportées sont: '
71
                    . implode(', ', config('app.supported_locales')))
0 ignored issues
show
Bug introduced by
config('app.supported_locales') of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

71
                    . implode(', ', /** @scrutinizer ignore-type */ config('app.supported_locales')))
Loading history...
72 2
                );
73
74 2
                return EXIT_USER_INPUT;
75
            }
76
77
            $currentLocale = $optionLocale;
78
        }
79
80
        if (is_string($optionDir)) {
81 2
            $tempCurrentDir = realpath($currentDir . $optionDir);
82
83
            if ($tempCurrentDir === false) {
84 2
                $this->error($this->color->error('Le dossier doit se trouvé dans "' . $currentDir . '"'));
85
86 2
                return EXIT_USER_INPUT;
87
            }
88
89
            if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) {
90
                $this->error($this->color->error('Le dossier "' . $this->languagePath . '" est restreint à l\'analyse.'));
91
92
                return EXIT_USER_INPUT;
93
            }
94
95
            $currentDir = $tempCurrentDir;
96
        }
97
98 2
        $this->process($currentDir, $currentLocale);
99
100 2
        $this->eol()->ok('Opérations terminées!');
101
102 2
        return EXIT_SUCCESS;
103
    }
104
105
    private function process(string $currentDir, string $currentLocale): void
106
    {
107 2
        $tableRows    = [];
108 2
        $countNewKeys = 0;
109
110 2
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir));
111 2
        $files    = iterator_to_array($iterator, true);
112 2
        ksort($files);
113
114
        [
115
            'foundLanguageKeys' => $foundLanguageKeys,
116
            'badLanguageKeys'   => $badLanguageKeys,
117
            'countFiles'        => $countFiles
118 2
        ] = $this->findLanguageKeysInFiles($files);
119
120 2
        ksort($foundLanguageKeys);
121
122 2
        $languageDiff        = [];
123 2
        $languageFoundGroups = array_unique(array_keys($foundLanguageKeys));
124
125
        foreach ($languageFoundGroups as $langFileName) {
126 2
            $languageStoredKeys = [];
127 2
            $languageFilePath   = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php';
128
129
            if (is_file($languageFilePath)) {
130
                // Charge les anciennes traductions
131
                $languageStoredKeys = require $languageFilePath;
132
            } elseif (! is_dir($dir = dirname($languageFilePath))) {
133
                // Si le dossier n'existe pas, on le cree
134
                @mkdir($dir, 0777, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

134
                /** @scrutinizer ignore-unhandled */ @mkdir($dir, 0777, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
135
            }
136
137 2
            $languageDiff = Arr::diffRecursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
138 2
            $countNewKeys += Arr::countRecursive($languageDiff);
139
140
            if ($this->showNew) {
141 2
                $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows);
142
            } else {
143 2
                $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
144
145
                if ($languageDiff !== []) {
146
                    if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) {
147
                        if ($this->verbose) {
148
                            $this->justify('Fichier de traduction "' . $langFileName . '"', 'Erreur lors de la modification', [
149
                                'second' => ['fg' => Color::RED],
150
                            ]);
151
                        }
152
                    } else {
153
                        if ($this->verbose) {
154
                            $this->justify('Fichier de traduction "' . $langFileName . '"', 'Modification éffectuée avec succès', [
155
                                'second' => ['fg' => Color::GREEN],
156 2
                            ]);
157
                        }
158
                    }
159
                }
160
            }
161
        }
162
163
        if ($this->showNew && $tableRows !== []) {
164 2
            sort($tableRows);
165 2
            $table = [];
166
167
            foreach ($tableRows as $body) {
168 2
                $table[] = array_combine(['Fichier', 'Clé'], $body);
169
            }
170 2
            $this->table($table);
171
        }
172
173
        if ($this->verbose) {
174 2
            $this->eol()->border(char: '*');
175
        }
176
177 2
        $this->justify('Fichiers analysés', $countFiles, ['second' => ['fg' => Color::GREEN]]);
178 2
        $this->justify('Nouvelles traductions trouvées', $countNewKeys, ['second' => ['fg' => Color::GREEN]]);
179 2
        $this->justify('Mauvaises traductions trouvées', count($badLanguageKeys), ['second' => ['fg' => Color::RED]]);
180
181
        if ($this->verbose && $badLanguageKeys !== []) {
182 2
            $tableBadRows = [];
183
184
            foreach ($badLanguageKeys as $value) {
185 2
                $tableBadRows[] = [$value[1], $value[0]];
186
            }
187
188 2
            usort($tableBadRows, static fn ($currentValue, $nextValue): int => strnatcmp((string) $currentValue[0], (string) $nextValue[0]));
189
190 2
            $table = [];
191
192
            foreach ($tableBadRows as $body) {
193 2
                $table[] = array_combine(['Mauvaise clé', 'Fichier'], $body);
194
            }
195
196 2
            $this->table($table);
197
        }
198
199
        if (! $this->showNew && $countNewKeys > 0) {
200 2
            $this->eol()->writer->bgRed('Note: Vous devez utiliser votre outil de linting pour résoudre les problèmes liés aux normes de codage.', true);
201
        }
202
    }
203
204
    /**
205
     * @param SplFileInfo|string $file
206
     *
207
     * @return array<string, array>
208
     */
209
    private function findTranslationsInFile($file): array
210
    {
211 2
        $foundLanguageKeys = [];
212 2
        $badLanguageKeys   = [];
213
214
        if (is_string($file) && is_file($file)) {
215 2
            $file = new SplFileInfo($file);
216
        }
217
218 2
        $fileContent = file_get_contents($file->getRealPath());
219
220 2
        preg_match_all('/\_\_\(\'([_a-z0-9À-ÿ\-]+)\'\)/ui', $fileContent, $matches);
221
222
        if ($matches[1] !== []) {
223 2
            $fileContent = str_replace($matches[0], array_map(static fn ($val) => "lang('App.{$val}')", $matches[1]), $fileContent);
224
        }
225
226 2
        preg_match_all('/lang\(\'([._a-z0-9À-ÿ\-]+)\'\)/ui', $fileContent, $matches);
227
228
        if ($matches[1] === []) {
229 2
            return compact('foundLanguageKeys', 'badLanguageKeys');
230
        }
231
232
        foreach ($matches[1] as $phraseKey) {
233 2
            $phraseKeys = explode('.', $phraseKey);
234
235
            // Le code langue n'a pas de nom de fichier ou de code langue.
236
            if (count($phraseKeys) < 2) {
237 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
238
239 2
                continue;
240
            }
241
242 2
            $languageFileName   = array_shift($phraseKeys);
243
            $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '')
244
                || ($languageFileName === '' && $phraseKeys[0] !== '')
245 2
                || ($languageFileName === '' && $phraseKeys[0] === '');
246
247
            if ($isEmptyNestedArray) {
248 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
249
250 2
                continue;
251
            }
252
253
            if (count($phraseKeys) === 1) {
254 2
                $foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey;
255
            } else {
256 2
                $childKeys = $this->buildMultiArray($phraseKeys, $phraseKey);
257
258 2
                $foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys);
259
            }
260
        }
261
262 2
        return compact('foundLanguageKeys', 'badLanguageKeys');
263
    }
264
265
    private function isIgnoredFile(SplFileInfo $file): bool
266
    {
267
        if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) {
268 2
            return true;
269
        }
270
271 2
        return $file->getExtension() !== 'php';
272
    }
273
274
    private function templateFile(array $language = []): string
275
    {
276
        if ($language !== []) {
277 2
            $languageArrayString = var_export($language, true);
278
279
            $code = <<<PHP
280
                <?php
281
282
                return {$languageArrayString};
283
284
                PHP;
285
286 2
            return $this->replaceArraySyntax($code);
287
        }
288
289
        return <<<'PHP'
290
            <?php
291
292
            return [];
293
294
            PHP;
295
    }
296
297
    private function replaceArraySyntax(string $code): string
298
    {
299 2
        $tokens    = token_get_all($code);
300 2
        $newTokens = $tokens;
301
302
        foreach ($tokens as $i => $token) {
303
            if (is_array($token)) {
304 2
                [$tokenId, $tokenValue] = $token;
305
306
                // Remplace "array (" par "["
307
                if (
308
                    $tokenId === T_ARRAY
309
                    && $tokens[$i + 1][0] === T_WHITESPACE
310
                    && $tokens[$i + 2] === '('
311
                ) {
312 2
                    $newTokens[$i][1]     = '[';
313 2
                    $newTokens[$i + 1][1] = '';
314 2
                    $newTokens[$i + 2]    = '';
315
                }
316
317
                // Remplace les indentations
318
                if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) {
319 2
                    $newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}";
320
                }
321
            } // Remplace ")"
322
            elseif ($token === ')') {
323 2
                $newTokens[$i] = ']';
324
            }
325
        }
326
327 2
        $output = '';
328
329
        foreach ($newTokens as $token) {
330 2
            $output .= $token[1] ?? $token;
331
        }
332
333 2
        return $output;
334
    }
335
336
    /**
337
     * Crée un tableau multidimensionnel à partir d'autres clés
338
     */
339
    private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array
340
    {
341 2
        $newArray  = [];
342 2
        $lastIndex = array_pop($fromKeys);
343 2
        $current   = &$newArray;
344
345
        foreach ($fromKeys as $value) {
346 2
            $current[$value] = [];
347 2
            $current         = &$current[$value];
348
        }
349
350 2
        $current[$lastIndex] = $lastArrayValue;
351
352 2
        return $newArray;
353
    }
354
355
    /**
356
     * Convertit les tableaux multidimensionnels en lignes de table CLI spécifiques (tableau plat)
357
     */
358
    private function arrayToTableRows(string $langFileName, array $array): array
359
    {
360 2
        $rows = [];
361
362
        foreach ($array as $value) {
363
            if (is_array($value)) {
364 2
                $rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value));
365
366 2
                continue;
367
            }
368
369
            if (is_string($value)) {
370 2
                $rows[] = [$langFileName, $value];
371
            }
372
        }
373
374 2
        return $rows;
375
    }
376
377
    private function isSubDirectory(string $directory, string $rootDirectory): bool
378
    {
379 2
        return 0 === strncmp($directory, $rootDirectory, strlen($directory));
380
    }
381
382
    /**
383
     * @param list<SplFileInfo> $files
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Cli\Commands\Translation\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
384
     *
385
     * @return         array<string, array|int>
386
     * @phpstan-return array{'foundLanguageKeys': array<string, array<string, string>>, 'badLanguageKeys': array<int, array<int, string>>, 'countFiles': int}
387
     */
388
    private function findLanguageKeysInFiles(array $files): array
389
    {
390 2
        $foundLanguageKeys = [];
391 2
        $badLanguageKeys   = [];
392 2
        $countFiles        = 0;
393
394
        foreach ($files as $file) {
395
            if ($this->isIgnoredFile($file)) {
396 2
                continue;
397
            }
398
399
            if ($this->verbose) {
400
                $this->justify(mb_substr($file->getRealPath(), mb_strlen(APP_PATH)), 'Analysé', [
401
                    'second' => ['fg' => Color::YELLOW],
402 2
                ]);
403
            }
404 2
            $countFiles++;
405
406 2
            $findInFile = $this->findTranslationsInFile($file);
407
408 2
            $foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys);
409 2
            $badLanguageKeys   = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys);
410
        }
411
412 2
        return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles');
413
    }
414
}
415