Passed
Push — main ( f6aab6...afae0a )
by Dimitri
07:52 queued 02:33
created

TranslationsFinder::execute()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.3387

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 29
c 2
b 1
f 0
nc 18
nop 1
dl 0
loc 51
ccs 17
cts 21
cp 0.8095
crap 7.3387
rs 8.5226

How to fix   Long Method   

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 BlitzPHP\Cli\Console\Command;
15
use BlitzPHP\Utilities\Iterable\Arr;
16
use Locale;
17
use RecursiveDirectoryIterator;
18
use RecursiveIteratorIterator;
19
use SplFileInfo;
20
21
/**
22
 * @credit <a href="https://codeigniter.com">CodeIgniter 4 - \CodeIgniter\Commands\Translation\LocalizationFinder</a>
23
 */
24
class TranslationsFinder extends Command
25
{
26
    protected $group       = 'Translation';
27
    protected $name        = 'translations:find';
28
    protected $description = 'Trouver et sauvegarder les phrases disponibles à traduire';
29
    protected $options     = [
30
        '--locale'   => 'Spécifier la locale (en, ru, etc.) pour enregistrer les fichiers',
31
        '--dir'      => 'Répertoire de recherche des traductions relatif à APP_PATH.',
32
        '--show-new' => 'N\'affiche que les nouvelles traductions dans le tableau. N\'écrit pas dans les fichiers.',
33
        '--verbose'  => 'Affiche des informations détaillées',
34
    ];
35
36
    /**
37
     * Indicateur pour afficher des informations détaillées
38
     */
39
    private bool $verbose = false;
40
41
    /**
42
     * Indicateur pour afficher uniquement les traductions, sans sauvegarde
43
     */
44
    private bool $showNew = false;
45
46
    private string $languagePath;
47
48
    /**
49
     * {@inheritDoc}
50
     */
51
    public function execute(array $params)
52
    {
53 2
        $this->verbose      = array_key_exists('verbose', $params);
54 2
        $this->showNew      = array_key_exists('show-new', $params);
55 2
        $optionLocale       = $params['locale'] ?? null;
56 2
        $optionDir          = $params['dir'] ?? null;
57 2
        $currentLocale      = Locale::getDefault();
58 2
        $currentDir         = APP_PATH;
59 2
        $this->languagePath = $currentDir . 'Translations';
60
61
        if (ENVIRONMENT === 'testing') {
0 ignored issues
show
Bug introduced by
The constant BlitzPHP\Cli\Commands\Translation\ENVIRONMENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
62 2
            $currentDir         = ROOTPATH . 'Services' . DS;
63 2
            $this->languagePath = ROOTPATH . 'Translations';
64
        }
65
66
        if (is_string($optionLocale)) {
67
            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

67
            if (! in_array($optionLocale, /** @scrutinizer ignore-type */ config('app.supported_locales'), true)) {
Loading history...
68
                $this->error(
69
                    'Erreur: "' . $optionLocale . '" n\'est pas supporté. Les langues supportées sont: '
70
                    . 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

70
                    . implode(', ', /** @scrutinizer ignore-type */ config('app.supported_locales'))
Loading history...
71 2
                );
72
73 2
                return EXIT_USER_INPUT;
74
            }
75
76
            $currentLocale = $optionLocale;
77
        }
78
79
        if (is_string($optionDir)) {
80 2
            $tempCurrentDir = realpath($currentDir . $optionDir);
81
82
            if ($tempCurrentDir === false) {
83 2
                $this->error('Erreur: Le dossier doit se trouvé dans "' . $currentDir . '"');
84
85 2
                return EXIT_USER_INPUT;
86
            }
87
88
            if ($this->isSubDirectory($tempCurrentDir, $this->languagePath)) {
89
                $this->error('Erreur: Le dossier "' . $this->languagePath . '" est restreint à l\'analyse.');
90
91
                return EXIT_USER_INPUT;
92
            }
93
94
            $currentDir = $tempCurrentDir;
95
        }
96
97 2
        $this->process($currentDir, $currentLocale);
98
99 2
        $this->ok('Opérations terminées!');
100
101 2
        return EXIT_SUCCESS;
102
    }
103
104
    private function process(string $currentDir, string $currentLocale): void
105
    {
106 2
        $tableRows    = [];
107 2
        $countNewKeys = 0;
108
109 2
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($currentDir));
110 2
        $files    = iterator_to_array($iterator, true);
111 2
        ksort($files);
112
113
        [
114
            'foundLanguageKeys' => $foundLanguageKeys,
115
            'badLanguageKeys'   => $badLanguageKeys,
116
            'countFiles'        => $countFiles
117 2
        ] = $this->findLanguageKeysInFiles($files);
118
119 2
        ksort($foundLanguageKeys);
120
121 2
        $languageDiff        = [];
122 2
        $languageFoundGroups = array_unique(array_keys($foundLanguageKeys));
123
124
        foreach ($languageFoundGroups as $langFileName) {
125 2
            $languageStoredKeys = [];
126 2
            $languageFilePath   = $this->languagePath . DIRECTORY_SEPARATOR . $currentLocale . DIRECTORY_SEPARATOR . $langFileName . '.php';
127
128
            if (is_file($languageFilePath)) {
129
                // Charge les anciennes traductions
130
                $languageStoredKeys = require $languageFilePath;
131
            }
132
133 2
            $languageDiff = Arr::diffRecursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
134 2
            $countNewKeys += Arr::countRecursive($languageDiff);
135
136
            if ($this->showNew) {
137 2
                $tableRows = array_merge($this->arrayToTableRows($langFileName, $languageDiff), $tableRows);
138
            } else {
139 2
                $newLanguageKeys = array_replace_recursive($foundLanguageKeys[$langFileName], $languageStoredKeys);
140
141
                if ($languageDiff !== []) {
142
                    if (file_put_contents($languageFilePath, $this->templateFile($newLanguageKeys)) === false) {
143 2
                        $this->writeIsVerbose('Fichier de traduction ' . $langFileName . ' (error write).', 'red');
144
                    } else {
145 2
                        $this->writeIsVerbose('Le fichier de traduction "' . $langFileName . '" a été modifié avec succès!', 'green');
146
                    }
147
                }
148
            }
149
        }
150
151
        if ($this->showNew && $tableRows !== []) {
152 2
            sort($tableRows);
153 2
            $table = [];
154
155
            foreach ($tableRows as $body) {
156 2
                $table[] = array_combine(['File', 'Key'], $body);
157
            }
158 2
            $this->table($table);
159
        }
160
161
        if (! $this->showNew && $countNewKeys > 0) {
162 2
            $this->writer->bgRed('Note: Vous devez utiliser votre outil de linting pour résoudre les problèmes liés aux normes de codage.');
163
        }
164
165 2
        $this->writeIsVerbose('Fichiers trouvés: ' . $countFiles);
166 2
        $this->writeIsVerbose('Nouvelles traductions trouvées: ' . $countNewKeys);
167 2
        $this->writeIsVerbose('Mauvaises traductions trouvées: ' . count($badLanguageKeys));
168
169
        if ($this->verbose && $badLanguageKeys !== []) {
170 2
            $tableBadRows = [];
171
172
            foreach ($badLanguageKeys as $value) {
173 2
                $tableBadRows[] = [$value[1], $value[0]];
174
            }
175
176 2
            usort($tableBadRows, static fn ($currentValue, $nextValue): int => strnatcmp((string) $currentValue[0], (string) $nextValue[0]));
177
178 2
            $table = [];
179
180
            foreach ($tableBadRows as $body) {
181 2
                $table[] = array_combine(['Bad Key', 'Filepath'], $body);
182
            }
183
184 2
            $this->table($table);
185
        }
186
    }
187
188
    /**
189
     * @param SplFileInfo|string $file
190
     *
191
     * @return array<string, array>
192
     */
193
    private function findTranslationsInFile($file): array
194
    {
195 2
        $foundLanguageKeys = [];
196 2
        $badLanguageKeys   = [];
197
198
        if (is_string($file) && is_file($file)) {
199 2
            $file = new SplFileInfo($file);
200
        }
201
202 2
        $fileContent = file_get_contents($file->getRealPath());
203 2
        preg_match_all('/lang\(\'([._a-z0-9\-]+)\'\)/ui', $fileContent, $matches);
204
205
        if ($matches[1] === []) {
206 2
            return compact('foundLanguageKeys', 'badLanguageKeys');
207
        }
208
209
        foreach ($matches[1] as $phraseKey) {
210 2
            $phraseKeys = explode('.', $phraseKey);
211
212
            // Le code langue n'a pas de nom de fichier ou de code langue.
213
            if (count($phraseKeys) < 2) {
214 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
215
216 2
                continue;
217
            }
218
219 2
            $languageFileName   = array_shift($phraseKeys);
220
            $isEmptyNestedArray = ($languageFileName !== '' && $phraseKeys[0] === '')
221
                || ($languageFileName === '' && $phraseKeys[0] !== '')
222 2
                || ($languageFileName === '' && $phraseKeys[0] === '');
223
224
            if ($isEmptyNestedArray) {
225 2
                $badLanguageKeys[] = [mb_substr($file->getRealPath(), mb_strlen(ROOTPATH)), $phraseKey];
226
227 2
                continue;
228
            }
229
230
            if (count($phraseKeys) === 1) {
231 2
                $foundLanguageKeys[$languageFileName][$phraseKeys[0]] = $phraseKey;
232
            } else {
233 2
                $childKeys = $this->buildMultiArray($phraseKeys, $phraseKey);
234
235 2
                $foundLanguageKeys[$languageFileName] = array_replace_recursive($foundLanguageKeys[$languageFileName] ?? [], $childKeys);
236
            }
237
        }
238
239 2
        return compact('foundLanguageKeys', 'badLanguageKeys');
240
    }
241
242
    private function isIgnoredFile(SplFileInfo $file): bool
243
    {
244
        if ($file->isDir() || $this->isSubDirectory($file->getRealPath(), $this->languagePath)) {
245 2
            return true;
246
        }
247
248 2
        return $file->getExtension() !== 'php';
249
    }
250
251
    private function templateFile(array $language = []): string
252
    {
253
        if ($language !== []) {
254 2
            $languageArrayString = var_export($language, true);
255
256
            $code = <<<PHP
257
                <?php
258
259
                return {$languageArrayString};
260
261
                PHP;
262
263 2
            return $this->replaceArraySyntax($code);
264
        }
265
266
        return <<<'PHP'
267
            <?php
268
269
            return [];
270
271
            PHP;
272
    }
273
274
    private function replaceArraySyntax(string $code): string
275
    {
276 2
        $tokens    = token_get_all($code);
277 2
        $newTokens = $tokens;
278
279
        foreach ($tokens as $i => $token) {
280
            if (is_array($token)) {
281 2
                [$tokenId, $tokenValue] = $token;
282
283
                // Remplace "array (" par "["
284
                if (
285
                    $tokenId === T_ARRAY
286
                    && $tokens[$i + 1][0] === T_WHITESPACE
287
                    && $tokens[$i + 2] === '('
288
                ) {
289 2
                    $newTokens[$i][1]     = '[';
290 2
                    $newTokens[$i + 1][1] = '';
291 2
                    $newTokens[$i + 2]    = '';
292
                }
293
294
                // Remplace les indentations
295
                if ($tokenId === T_WHITESPACE && preg_match('/\n([ ]+)/u', $tokenValue, $matches)) {
296 2
                    $newTokens[$i][1] = "\n{$matches[1]}{$matches[1]}";
297
                }
298
            } // Remplace ")"
299
            elseif ($token === ')') {
300 2
                $newTokens[$i] = ']';
301
            }
302
        }
303
304 2
        $output = '';
305
306
        foreach ($newTokens as $token) {
307 2
            $output .= $token[1] ?? $token;
308
        }
309
310 2
        return $output;
311
    }
312
313
    /**
314
     * Crée un tableau multidimensionnel à partir d'autres clés
315
     */
316
    private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array
317
    {
318 2
        $newArray  = [];
319 2
        $lastIndex = array_pop($fromKeys);
320 2
        $current   = &$newArray;
321
322
        foreach ($fromKeys as $value) {
323 2
            $current[$value] = [];
324 2
            $current         = &$current[$value];
325
        }
326
327 2
        $current[$lastIndex] = $lastArrayValue;
328
329 2
        return $newArray;
330
    }
331
332
    /**
333
     * Convertit les tableaux multidimensionnels en lignes de table CLI spécifiques (tableau plat)
334
     */
335
    private function arrayToTableRows(string $langFileName, array $array): array
336
    {
337 2
        $rows = [];
338
339
        foreach ($array as $value) {
340
            if (is_array($value)) {
341 2
                $rows = array_merge($rows, $this->arrayToTableRows($langFileName, $value));
342
343 2
                continue;
344
            }
345
346
            if (is_string($value)) {
347 2
                $rows[] = [$langFileName, $value];
348
            }
349
        }
350
351 2
        return $rows;
352
    }
353
354
    /**
355
     * Affiche les détails dans la console si l'indicateur est défini
356
     */
357
    private function writeIsVerbose(string $text = '', ?string $foreground = null, ?string $background = null): void
358
    {
359
        if ($this->verbose) {
360 2
            $this->write($this->color->line($text, ['fg' => $foreground, 'bg' => $background]));
361
        }
362
    }
363
364
    private function isSubDirectory(string $directory, string $rootDirectory): bool
365
    {
366 2
        return 0 === strncmp($directory, $rootDirectory, strlen($directory));
367
    }
368
369
    /**
370
     * @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...
371
     *
372
     * @return         array<string, array|int>
373
     * @phpstan-return array{'foundLanguageKeys': array<string, array<string, string>>, 'badLanguageKeys': array<int, array<int, string>>, 'countFiles': int}
374
     */
375
    private function findLanguageKeysInFiles(array $files): array
376
    {
377 2
        $foundLanguageKeys = [];
378 2
        $badLanguageKeys   = [];
379 2
        $countFiles        = 0;
380
381
        foreach ($files as $file) {
382
            if ($this->isIgnoredFile($file)) {
383 2
                continue;
384
            }
385
386 2
            $this->writeIsVerbose('Ficher trouvé: ' . mb_substr($file->getRealPath(), mb_strlen(APP_PATH)));
387 2
            $countFiles++;
388
389 2
            $findInFile = $this->findTranslationsInFile($file);
390
391 2
            $foundLanguageKeys = array_replace_recursive($findInFile['foundLanguageKeys'], $foundLanguageKeys);
392 2
            $badLanguageKeys   = array_merge($findInFile['badLanguageKeys'], $badLanguageKeys);
393
        }
394
395 2
        return compact('foundLanguageKeys', 'badLanguageKeys', 'countFiles');
396
    }
397
}
398