GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( bbf2ff...d7ffda )
by Robert
12:47
created

MessageController::extractMessagesFromTokens()   D

Complexity

Conditions 21
Paths 15

Size

Total Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 21.1145

Importance

Changes 0
Metric Value
dl 0
loc 83
ccs 44
cts 47
cp 0.9362
rs 4.1666
c 0
b 0
f 0
cc 21
nc 15
nop 3
crap 21.1145

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
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\console\Exception;
12
use yii\console\ExitCode;
13
use yii\db\Connection;
14
use yii\db\Query;
15
use yii\di\Instance;
16
use yii\helpers\Console;
17
use yii\helpers\FileHelper;
18
use yii\helpers\VarDumper;
19
use yii\i18n\GettextPoFile;
20
21
/**
22
 * Extracts messages to be translated from source files.
23
 *
24
 * The extracted messages can be saved the following depending on `format`
25
 * setting in config file:
26
 *
27
 * - PHP message source files.
28
 * - ".po" files.
29
 * - Database.
30
 *
31
 * Usage:
32
 * 1. Create a configuration file using the 'message/config' command:
33
 *    yii message/config /path/to/myapp/messages/config.php
34
 * 2. Edit the created config file, adjusting it for your web application needs.
35
 * 3. Run the 'message/extract' command, using created config:
36
 *    yii message /path/to/myapp/messages/config.php
37
 *
38
 * @author Qiang Xue <[email protected]>
39
 * @since 2.0
40
 */
41
class MessageController extends \yii\console\Controller
42
{
43
    /**
44
     * @var string controller default action ID.
45
     */
46
    public $defaultAction = 'extract';
47
    /**
48
     * @var string required, root directory of all source files.
49
     */
50
    public $sourcePath = '@yii';
51
    /**
52
     * @var string required, root directory containing message translations.
53
     */
54
    public $messagePath = '@yii/messages';
55
    /**
56
     * @var array required, list of language codes that the extracted messages
57
     * should be translated to. For example, ['zh-CN', 'de'].
58
     */
59
    public $languages = [];
60
    /**
61
     * @var string the name of the function for translating messages.
62
     * Defaults to 'Yii::t'. This is used as a mark to find the messages to be
63
     * translated. You may use a string for single function name or an array for
64
     * multiple function names.
65
     */
66
    public $translator = 'Yii::t';
67
    /**
68
     * @var bool whether to sort messages by keys when merging new messages
69
     * with the existing ones. Defaults to false, which means the new (untranslated)
70
     * messages will be separated from the old (translated) ones.
71
     */
72
    public $sort = false;
73
    /**
74
     * @var bool whether the message file should be overwritten with the merged messages
75
     */
76
    public $overwrite = true;
77
    /**
78
     * @var bool whether to remove messages that no longer appear in the source code.
79
     * Defaults to false, which means these messages will NOT be removed.
80
     */
81
    public $removeUnused = false;
82
    /**
83
     * @var bool whether to mark messages that no longer appear in the source code.
84
     * Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
85
     */
86
    public $markUnused = true;
87
    /**
88
     * @var array list of patterns that specify which files/directories should NOT be processed.
89
     * If empty or not set, all files/directories will be processed.
90
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
91
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
92
     */
93
    public $except = [
94
        '.svn',
95
        '.git',
96
        '.gitignore',
97
        '.gitkeep',
98
        '.hgignore',
99
        '.hgkeep',
100
        '/messages',
101
        '/BaseYii.php', // contains examples about Yii:t()
102
    ];
103
    /**
104
     * @var array list of patterns that specify which files (not directories) should be processed.
105
     * If empty or not set, all files will be processed.
106
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
107
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
108
     */
109
    public $only = ['*.php'];
110
    /**
111
     * @var string generated file format. Can be "php", "db", "po" or "pot".
112
     */
113
    public $format = 'php';
114
    /**
115
     * @var string connection component ID for "db" format.
116
     */
117
    public $db = 'db';
118
    /**
119
     * @var string custom name for source message table for "db" format.
120
     */
121
    public $sourceMessageTable = '{{%source_message}}';
122
    /**
123
     * @var string custom name for translation message table for "db" format.
124
     */
125
    public $messageTable = '{{%message}}';
126
    /**
127
     * @var string name of the file that will be used for translations for "po" format.
128
     */
129
    public $catalog = 'messages';
130
    /**
131
     * @var array message categories to ignore. For example, 'yii', 'app*', 'widgets/menu', etc.
132
     * @see isCategoryIgnored
133
     */
134
    public $ignoreCategories = [];
135
    /**
136
     * @var string File header in generated PHP file with messages. This property is used only if [[$format]] is "php".
137
     * @since 2.0.13
138
     */
139
    public $phpFileHeader = '';
140
    /**
141
     * @var string|null DocBlock used for messages array in generated PHP file. If `null`, default DocBlock will be used.
142
     * This property is used only if [[$format]] is "php".
143
     * @since 2.0.13
144
     */
145
    public $phpDocBlock;
146
147
    /**
148
     * @var array Config for messages extraction.
149
     * @see actionExtract()
150
     * @see initConfig()
151
     * @since 2.0.13
152
     */
153
    protected $config;
154
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 48
    public function options($actionID)
160
    {
161 48
        return array_merge(parent::options($actionID), [
162 48
            'sourcePath',
163
            'messagePath',
164
            'languages',
165
            'translator',
166
            'sort',
167
            'overwrite',
168
            'removeUnused',
169
            'markUnused',
170
            'except',
171
            'only',
172
            'format',
173
            'db',
174
            'sourceMessageTable',
175
            'messageTable',
176
            'catalog',
177
            'ignoreCategories',
178
            'phpFileHeader',
179
            'phpDocBlock',
180
        ]);
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     * @since 2.0.8
186
     */
187
    public function optionAliases()
188
    {
189
        return array_merge(parent::optionAliases(), [
190
            'c' => 'catalog',
191
            'e' => 'except',
192
            'f' => 'format',
193
            'i' => 'ignoreCategories',
194
            'l' => 'languages',
195
            'u' => 'markUnused',
196
            'p' => 'messagePath',
197
            'o' => 'only',
198
            'w' => 'overwrite',
199
            'S' => 'sort',
200
            't' => 'translator',
201
            'm' => 'sourceMessageTable',
202
            's' => 'sourcePath',
203
            'r' => 'removeUnused',
204
        ]);
205
    }
206
207
    /**
208
     * Creates a configuration file for the "extract" command using command line options specified.
209
     *
210
     * The generated configuration file contains parameters required
211
     * for source code messages extraction.
212
     * You may use this configuration file with the "extract" command.
213
     *
214
     * @param string $filePath output file name or alias.
215
     * @return int CLI exit code
216
     * @throws Exception on failure.
217
     */
218 3
    public function actionConfig($filePath)
219
    {
220 3
        $filePath = Yii::getAlias($filePath);
221 3
        if (file_exists($filePath)) {
222
            if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm("File '{$...wish to overwrite it?") of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
223
                return ExitCode::OK;
224
            }
225
        }
226
227 3
        $array = VarDumper::export($this->getOptionValues($this->action->id));
228
        $content = <<<EOD
229
<?php
230
/**
231 3
 * Configuration file for 'yii {$this->id}/{$this->defaultAction}' command.
232
 *
233 3
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
234
 * It contains parameters for source code messages extraction.
235
 * You may modify this file to suit your needs.
236
 *
237 3
 * You can use 'yii {$this->id}/{$this->action->id}-template' command to create
238
 * template configuration file with detailed description for each parameter.
239
 */
240 3
return $array;
241
242
EOD;
243
244 3
        if (file_put_contents($filePath, $content, LOCK_EX) === false) {
245
            $this->stdout("Configuration file was NOT created: '{$filePath}'.\n\n", Console::FG_RED);
246
            return ExitCode::UNSPECIFIED_ERROR;
247
        }
248
249 3
        $this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN);
250 3
        return ExitCode::OK;
251
    }
252
253
    /**
254
     * Creates a configuration file template for the "extract" command.
255
     *
256
     * The created configuration file contains detailed instructions on
257
     * how to customize it to fit for your needs. After customization,
258
     * you may use this configuration file with the "extract" command.
259
     *
260
     * @param string $filePath output file name or alias.
261
     * @return int CLI exit code
262
     * @throws Exception on failure.
263
     */
264
    public function actionConfigTemplate($filePath)
265
    {
266
        $filePath = Yii::getAlias($filePath);
267
268
        if (file_exists($filePath)) {
269
            if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm("File '{$...wish to overwrite it?") of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
270
                return ExitCode::OK;
271
            }
272
        }
273
274
        if (!copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath)) {
275
            $this->stdout("Configuration file template was NOT created at '{$filePath}'.\n\n", Console::FG_RED);
276
            return ExitCode::UNSPECIFIED_ERROR;
277
        }
278
279
        $this->stdout("Configuration file template created at '{$filePath}'.\n\n", Console::FG_GREEN);
280
        return ExitCode::OK;
281
    }
282
283
    /**
284
     * Extracts messages to be translated from source code.
285
     *
286
     * This command will search through source code files and extract
287
     * messages that need to be translated in different languages.
288
     *
289
     * @param string $configFile the path or alias of the configuration file.
290
     * You may use the "yii message/config" command to generate
291
     * this file and then customize it for your needs.
292
     * @throws Exception on failure.
293
     */
294 45
    public function actionExtract($configFile = null)
295
    {
296 45
        $this->initConfig($configFile);
297
298 42
        $files = FileHelper::findFiles(realpath($this->config['sourcePath']), $this->config);
299
300 42
        $messages = [];
301 42
        foreach ($files as $file) {
302 42
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $this->config['translator'], $this->config['ignoreCategories']));
303
        }
304
305 42
        $catalog = isset($this->config['catalog']) ? $this->config['catalog'] : 'messages';
306
307 42
        if (in_array($this->config['format'], ['php', 'po'])) {
308 29
            foreach ($this->config['languages'] as $language) {
309 29
                $dir = $this->config['messagePath'] . DIRECTORY_SEPARATOR . $language;
310 29
                if (!is_dir($dir) && !@mkdir($dir)) {
311
                    throw new Exception("Directory '{$dir}' can not be created.");
312
                }
313 29
                if ($this->config['format'] === 'po') {
314 14
                    $this->saveMessagesToPO($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $catalog, $this->config['markUnused']);
315
                } else {
316 29
                    $this->saveMessagesToPHP($messages, $dir, $this->config['overwrite'], $this->config['removeUnused'], $this->config['sort'], $this->config['markUnused']);
317
                }
318
            }
319 13
        } elseif ($this->config['format'] === 'db') {
320
            /** @var Connection $db */
321 13
            $db = Instance::ensure($this->config['db'], Connection::className());
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
322 13
            $sourceMessageTable = isset($this->config['sourceMessageTable']) ? $this->config['sourceMessageTable'] : '{{%source_message}}';
323 13
            $messageTable = isset($this->config['messageTable']) ? $this->config['messageTable'] : '{{%message}}';
324 13
            $this->saveMessagesToDb(
325 13
                $messages,
326 13
                $db,
327 13
                $sourceMessageTable,
328 13
                $messageTable,
329 13
                $this->config['removeUnused'],
330 13
                $this->config['languages'],
331 13
                $this->config['markUnused']
332
            );
333
        } elseif ($this->config['format'] === 'pot') {
334
            $this->saveMessagesToPOT($messages, $this->config['messagePath'], $catalog);
335
        }
336 42
    }
337
338
    /**
339
     * Saves messages to database.
340
     *
341
     * @param array $messages
342
     * @param Connection $db
343
     * @param string $sourceMessageTable
344
     * @param string $messageTable
345
     * @param bool $removeUnused
346
     * @param array $languages
347
     * @param bool $markUnused
348
     */
349 13
    protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused)
350
    {
351 13
        $currentMessages = [];
352 13
        $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
353 13
        foreach ($rows as $row) {
354 12
            $currentMessages[$row['category']][$row['id']] = $row['message'];
355
        }
356
357 13
        $currentLanguages = [];
358 13
        $rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db);
359 13
        foreach ($rows as $row) {
360 12
            $currentLanguages[] = $row['language'];
361
        }
362 13
        $missingLanguages = [];
363 13
        if (!empty($currentLanguages)) {
364 12
            $missingLanguages = array_diff($languages, $currentLanguages);
365
        }
366
367 13
        $new = [];
368 13
        $obsolete = [];
369
370 13
        foreach ($messages as $category => $msgs) {
371 13
            $msgs = array_unique($msgs);
372
373 13
            if (isset($currentMessages[$category])) {
374 9
                $new[$category] = array_diff($msgs, $currentMessages[$category]);
375 9
                $obsolete += array_diff($currentMessages[$category], $msgs);
376
            } else {
377 13
                $new[$category] = $msgs;
378
            }
379
        }
380
381 13
        foreach (array_diff(array_keys($currentMessages), array_keys($messages)) as $category) {
382 8
            $obsolete += $currentMessages[$category];
383
        }
384
385 13
        if (!$removeUnused) {
386 12
            foreach ($obsolete as $pk => $msg) {
387 9
                if (mb_substr($msg, 0, 2) === '@@' && mb_substr($msg, -2) === '@@') {
388 9
                    unset($obsolete[$pk]);
389
                }
390
            }
391
        }
392
393 13
        $obsolete = array_keys($obsolete);
394 13
        $this->stdout('Inserting new messages...');
395 13
        $savedFlag = false;
396
397 13
        foreach ($new as $category => $msgs) {
398 13
            foreach ($msgs as $msg) {
399 11
                $savedFlag = true;
400 11
                $lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]);
401 11
                foreach ($languages as $language) {
402 11
                    $db->createCommand()
403 11
                       ->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language])
404 13
                       ->execute();
405
                }
406
            }
407
        }
408
409 13
        if (!empty($missingLanguages)) {
410 1
            $updatedMessages = [];
411 1
            $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db);
412 1
            foreach ($rows as $row) {
413 1
                $updatedMessages[$row['category']][$row['id']] = $row['message'];
414
            }
415 1
            foreach ($updatedMessages as $category => $msgs) {
416 1
                foreach ($msgs as $id => $msg) {
417 1
                    $savedFlag = true;
418 1
                    foreach ($missingLanguages as $language) {
419 1
                        $db->createCommand()
420 1
                            ->insert($messageTable, ['id' => $id, 'language' => $language])
421 1
                            ->execute();
422
                    }
423
                }
424
            }
425
        }
426
427 13
        $this->stdout($savedFlag ? "saved.\n" : "Nothing to save.\n");
428 13
        $this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...');
429
430 13
        if (empty($obsolete)) {
431 6
            $this->stdout("Nothing obsoleted...skipped.\n");
432 6
            return;
433
        }
434
435 10
        if ($removeUnused) {
436 1
            $db->createCommand()
437 1
               ->delete($sourceMessageTable, ['in', 'id', $obsolete])
438 1
               ->execute();
439 1
            $this->stdout("deleted.\n");
440 9
        } elseif ($markUnused) {
441 7
            $rows = (new Query())
442 7
                ->select(['id', 'message'])
443 7
                ->from($sourceMessageTable)
444 7
                ->where(['in', 'id', $obsolete])
445 7
                ->all($db);
446
447 7
            foreach ($rows as $row) {
448 7
                $db->createCommand()->update(
449 7
                    $sourceMessageTable,
450 7
                    ['message' => '@@' . $row['message'] . '@@'],
451 7
                    ['id' => $row['id']]
452 7
                )->execute();
453
            }
454 7
            $this->stdout("updated.\n");
455
        } else {
456 2
            $this->stdout("kept untouched.\n");
457
        }
458 10
    }
459
460
    /**
461
     * Extracts messages from a file.
462
     *
463
     * @param string $fileName name of the file to extract messages from
464
     * @param string $translator name of the function used to translate messages
465
     * @param array $ignoreCategories message categories to ignore.
466
     * This parameter is available since version 2.0.4.
467
     * @return array
468
     */
469 42
    protected function extractMessages($fileName, $translator, $ignoreCategories = [])
470
    {
471 42
        $this->stdout('Extracting messages from ');
472 42
        $this->stdout($fileName, Console::FG_CYAN);
473 42
        $this->stdout("...\n");
474
475 42
        $subject = file_get_contents($fileName);
476 42
        $messages = [];
477 42
        $tokens = token_get_all($subject);
478 42
        foreach ((array) $translator as $currentTranslator) {
479 42
            $translatorTokens = token_get_all('<?php ' . $currentTranslator);
480 42
            array_shift($translatorTokens);
481 42
            $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($tokens, $translatorTokens, $ignoreCategories));
482
        }
483
484 42
        $this->stdout("\n");
485
486 42
        return $messages;
487
    }
488
489
    /**
490
     * Extracts messages from a parsed PHP tokens list.
491
     * @param array $tokens tokens to be processed.
492
     * @param array $translatorTokens translator tokens.
493
     * @param array $ignoreCategories message categories to ignore.
494
     * @return array messages.
495
     */
496 42
    protected function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories)
497
    {
498 42
        $messages = [];
499 42
        $translatorTokensCount = count($translatorTokens);
500 42
        $matchedTokensCount = 0;
501 42
        $buffer = [];
502 42
        $pendingParenthesisCount = 0;
503
504 42
        foreach ($tokens as $i => $token) {
505
            // finding out translator call
506 42
            if ($matchedTokensCount < $translatorTokensCount) {
507 42
                if ($this->tokensEqual($token, $translatorTokens[$matchedTokensCount])) {
508 42
                    $matchedTokensCount++;
509
                } else {
510 42
                    $matchedTokensCount = 0;
511
                }
512 42
            } elseif ($matchedTokensCount === $translatorTokensCount) {
513
                // translator found
514
515
                // end of function call
516 42
                if ($this->tokensEqual(')', $token)) {
517 42
                    $pendingParenthesisCount--;
518
519 42
                    if ($pendingParenthesisCount === 0) {
520
                        // end of translator call or end of something that we can't extract
521 42
                        if (isset($buffer[0][0], $buffer[1], $buffer[2][0]) && $buffer[0][0] === T_CONSTANT_ENCAPSED_STRING && $buffer[1] === ',' && $buffer[2][0] === T_CONSTANT_ENCAPSED_STRING) {
522
                            // is valid call we can extract
523 42
                            $category = stripcslashes($buffer[0][1]);
524 42
                            $category = mb_substr($category, 1, -1);
525
526 42
                            if (!$this->isCategoryIgnored($category, $ignoreCategories)) {
527 42
                                $fullMessage = mb_substr($buffer[2][1], 1, -1);
528 42
                                $i = 3;
529 42
                                while ($i < count($buffer) - 1 && !is_array($buffer[$i]) && $buffer[$i] === '.') {
530 3
                                    $fullMessage .= mb_substr($buffer[$i + 1][1], 1, -1);
531 3
                                    $i += 2;
532
                                }
533
534 42
                                $message = stripcslashes($fullMessage);
535 42
                                $messages[$category][] = $message;
536
                            }
537
538 42
                            $nestedTokens = array_slice($buffer, 3);
539 42
                            if (count($nestedTokens) > $translatorTokensCount) {
540
                                // search for possible nested translator calls
541 42
                                $messages = array_merge_recursive($messages, $this->extractMessagesFromTokens($nestedTokens, $translatorTokens, $ignoreCategories));
542
                            }
543
                        } else {
544
                            // invalid call or dynamic call we can't extract
545
                            $line = Console::ansiFormat($this->getLine($buffer), [Console::FG_CYAN]);
546
                            $skipping = Console::ansiFormat('Skipping line', [Console::FG_YELLOW]);
547
                            $this->stdout("$skipping $line. Make sure both category and message are static strings.\n");
548
                        }
549
550
                        // prepare for the next match
551 42
                        $matchedTokensCount = 0;
552 42
                        $pendingParenthesisCount = 0;
553 42
                        $buffer = [];
554
                    } else {
555 42
                        $buffer[] = $token;
556
                    }
557 42
                } elseif ($this->tokensEqual('(', $token)) {
558
                    // count beginning of function call, skipping translator beginning
559
                    // Ensure that it's not the call of the object method. See https://github.com/yiisoft/yii2/issues/16828
560 42
                    $previousTokenId = $tokens[$i - $matchedTokensCount - 1][0];
561 42
                    if (in_array($previousTokenId, [T_OBJECT_OPERATOR, T_PAAMAYIM_NEKUDOTAYIM], true)) {
562 3
                        $matchedTokensCount = 0;
563 3
                        continue;
564
                    }
565
566 42
                    if ($pendingParenthesisCount > 0) {
567 3
                        $buffer[] = $token;
568
                    }
569 42
                    $pendingParenthesisCount++;
570 42
                } elseif (isset($token[0]) && !in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
571
                    // ignore comments and whitespaces
572 42
                    $buffer[] = $token;
573
                }
574
            }
575
        }
576
577 42
        return $messages;
578
    }
579
580
    /**
581
     * The method checks, whether the $category is ignored according to $ignoreCategories array.
582
     *
583
     * Examples:
584
     *
585
     * - `myapp` - will be ignored only `myapp` category;
586
     * - `myapp*` - will be ignored by all categories beginning with `myapp` (`myapp`, `myapplication`, `myapprove`, `myapp/widgets`, `myapp.widgets`, etc).
587
     *
588
     * @param string $category category that is checked
589
     * @param array $ignoreCategories message categories to ignore.
590
     * @return bool
591
     * @since 2.0.7
592
     */
593 42
    protected function isCategoryIgnored($category, array $ignoreCategories)
594
    {
595 42
        if (!empty($ignoreCategories)) {
596 3
            if (in_array($category, $ignoreCategories, true)) {
597 3
                return true;
598
            }
599 3
            foreach ($ignoreCategories as $pattern) {
600 3
                if (strpos($pattern, '*') > 0 && strpos($category, rtrim($pattern, '*')) === 0) {
601 3
                    return true;
602
                }
603
            }
604
        }
605
606 42
        return false;
607
    }
608
609
    /**
610
     * Finds out if two PHP tokens are equal.
611
     *
612
     * @param array|string $a
613
     * @param array|string $b
614
     * @return bool
615
     * @since 2.0.1
616
     */
617 42
    protected function tokensEqual($a, $b)
618
    {
619 42
        if (is_string($a) && is_string($b)) {
620 42
            return $a === $b;
621
        }
622 42
        if (isset($a[0], $a[1], $b[0], $b[1])) {
623 42
            return $a[0] === $b[0] && $a[1] == $b[1];
624
        }
625
626 42
        return false;
627
    }
628
629
    /**
630
     * Finds out a line of the first non-char PHP token found.
631
     *
632
     * @param array $tokens
633
     * @return int|string
634
     * @since 2.0.1
635
     */
636
    protected function getLine($tokens)
637
    {
638
        foreach ($tokens as $token) {
639
            if (isset($token[2])) {
640
                return $token[2];
641
            }
642
        }
643
644
        return 'unknown';
645
    }
646
647
    /**
648
     * Writes messages into PHP files.
649
     *
650
     * @param array $messages
651
     * @param string $dirName name of the directory to write to
652
     * @param bool $overwrite if existing file should be overwritten without backup
653
     * @param bool $removeUnused if obsolete translations should be removed
654
     * @param bool $sort if translations should be sorted
655
     * @param bool $markUnused if obsolete translations should be marked
656
     */
657 15
    protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort, $markUnused)
658
    {
659 15
        foreach ($messages as $category => $msgs) {
660 15
            $file = str_replace('\\', '/', "$dirName/$category.php");
661 15
            $path = dirname($file);
662 15
            FileHelper::createDirectory($path);
663 15
            $msgs = array_values(array_unique($msgs));
664 15
            $coloredFileName = Console::ansiFormat($file, [Console::FG_CYAN]);
665 15
            $this->stdout("Saving messages to $coloredFileName...\n");
666 15
            $this->saveMessagesCategoryToPHP($msgs, $file, $overwrite, $removeUnused, $sort, $category, $markUnused);
667
        }
668 15
    }
669
670
    /**
671
     * Writes category messages into PHP file.
672
     *
673
     * @param array $messages
674
     * @param string $fileName name of the file to write to
675
     * @param bool $overwrite if existing file should be overwritten without backup
676
     * @param bool $removeUnused if obsolete translations should be removed
677
     * @param bool $sort if translations should be sorted
678
     * @param string $category message category
679
     * @param bool $markUnused if obsolete translations should be marked
680
     * @return int exit code
681
     */
682 15
    protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $markUnused)
683
    {
684 15
        if (is_file($fileName)) {
685 9
            $rawExistingMessages = require $fileName;
686 9
            $existingMessages = $rawExistingMessages;
687 9
            sort($messages);
688 9
            ksort($existingMessages);
689 9
            if (array_keys($existingMessages) === $messages && (!$sort || array_keys($rawExistingMessages) === $messages)) {
690 4
                $this->stdout("Nothing new in \"$category\" category... Nothing to save.\n\n", Console::FG_GREEN);
691 4
                return ExitCode::OK;
692
            }
693 6
            unset($rawExistingMessages);
694 6
            $merged = [];
695 6
            $untranslated = [];
696 6
            foreach ($messages as $message) {
697 6
                if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
698 3
                    $merged[$message] = $existingMessages[$message];
699
                } else {
700 6
                    $untranslated[] = $message;
701
                }
702
            }
703 6
            ksort($merged);
704 6
            sort($untranslated);
705 6
            $todo = [];
706 6
            foreach ($untranslated as $message) {
707 5
                $todo[$message] = '';
708
            }
709 6
            ksort($existingMessages);
710 6
            foreach ($existingMessages as $message => $translation) {
711 6
                if (!$removeUnused && !isset($merged[$message]) && !isset($todo[$message])) {
712 3
                    if (!$markUnused || (!empty($translation) && (strncmp($translation, '@@', 2) === 0 && substr_compare($translation, '@@', -2, 2) === 0))) {
713 2
                        $todo[$message] = $translation;
714
                    } else {
715 6
                        $todo[$message] = '@@' . $translation . '@@';
716
                    }
717
                }
718
            }
719 6
            $merged = array_merge($todo, $merged);
720 6
            if ($sort) {
721
                ksort($merged);
722
            }
723 6
            if (false === $overwrite) {
724
                $fileName .= '.merged';
725
            }
726 6
            $this->stdout("Translation merged.\n");
727
        } else {
728 10
            $merged = [];
729 10
            foreach ($messages as $message) {
730 10
                $merged[$message] = '';
731
            }
732 10
            ksort($merged);
733
        }
734
735 15
        $array = VarDumper::export($merged);
736
        $content = <<<EOD
737
<?php
738 15
{$this->config['phpFileHeader']}{$this->config['phpDocBlock']}
739 15
return $array;
740
741
EOD;
742
743 15
        if (file_put_contents($fileName, $content, LOCK_EX) === false) {
744
            $this->stdout("Translation was NOT saved.\n\n", Console::FG_RED);
745
            return ExitCode::UNSPECIFIED_ERROR;
746
        }
747
748 15
        $this->stdout("Translation saved.\n\n", Console::FG_GREEN);
749 15
        return ExitCode::OK;
750
    }
751
752
    /**
753
     * Writes messages into PO file.
754
     *
755
     * @param array $messages
756
     * @param string $dirName name of the directory to write to
757
     * @param bool $overwrite if existing file should be overwritten without backup
758
     * @param bool $removeUnused if obsolete translations should be removed
759
     * @param bool $sort if translations should be sorted
760
     * @param string $catalog message catalog
761
     * @param bool $markUnused if obsolete translations should be marked
762
     */
763 14
    protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog, $markUnused)
764
    {
765 14
        $file = str_replace('\\', '/', "$dirName/$catalog.po");
766 14
        FileHelper::createDirectory(dirname($file));
767 14
        $this->stdout("Saving messages to $file...\n");
768
769 14
        $poFile = new GettextPoFile();
770
771 14
        $merged = [];
772 14
        $todos = [];
773
774 14
        $hasSomethingToWrite = false;
775 14
        foreach ($messages as $category => $msgs) {
776 14
            $notTranslatedYet = [];
777 14
            $msgs = array_values(array_unique($msgs));
778
779 14
            if (is_file($file)) {
780 9
                $existingMessages = $poFile->load($file, $category);
781
782 9
                sort($msgs);
783 9
                ksort($existingMessages);
784 9
                if (array_keys($existingMessages) == $msgs) {
785 4
                    $this->stdout("Nothing new in \"$category\" category...\n");
786
787 4
                    sort($msgs);
788 4
                    foreach ($msgs as $message) {
789 4
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
790
                    }
791 4
                    ksort($merged);
792 4
                    continue;
793
                }
794
795
                // merge existing message translations with new message translations
796 6
                foreach ($msgs as $message) {
797 6
                    if (array_key_exists($message, $existingMessages) && $existingMessages[$message] !== '') {
798 3
                        $merged[$category . chr(4) . $message] = $existingMessages[$message];
799
                    } else {
800 6
                        $notTranslatedYet[] = $message;
801
                    }
802
                }
803 6
                ksort($merged);
804 6
                sort($notTranslatedYet);
805
806
                // collect not yet translated messages
807 6
                foreach ($notTranslatedYet as $message) {
808 5
                    $todos[$category . chr(4) . $message] = '';
809
                }
810
811
                // add obsolete unused messages
812 6
                foreach ($existingMessages as $message => $translation) {
813 6
                    if (!$removeUnused && !isset($merged[$category . chr(4) . $message]) && !isset($todos[$category . chr(4) . $message])) {
814 3
                        if (!$markUnused || (!empty($translation) && (substr($translation, 0, 2) === '@@' && substr($translation, -2) === '@@'))) {
815 2
                            $todos[$category . chr(4) . $message] = $translation;
816
                        } else {
817 6
                            $todos[$category . chr(4) . $message] = '@@' . $translation . '@@';
818
                        }
819
                    }
820
                }
821
822 6
                $merged = array_merge($todos, $merged);
823 6
                if ($sort) {
824
                    ksort($merged);
825
                }
826
827 6
                if ($overwrite === false) {
828 6
                    $file .= '.merged';
829
                }
830
            } else {
831 9
                sort($msgs);
832 9
                foreach ($msgs as $message) {
833 9
                    $merged[$category . chr(4) . $message] = '';
834
                }
835 9
                ksort($merged);
836
            }
837 14
            $this->stdout("Category \"$category\" merged.\n");
838 14
            $hasSomethingToWrite = true;
839
        }
840 14
        if ($hasSomethingToWrite) {
841 14
            $poFile->save($file, $merged);
842 14
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
843
        } else {
844 3
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
845
        }
846 14
    }
847
848
    /**
849
     * Writes messages into POT file.
850
     *
851
     * @param array $messages
852
     * @param string $dirName name of the directory to write to
853
     * @param string $catalog message catalog
854
     * @since 2.0.6
855
     */
856
    protected function saveMessagesToPOT($messages, $dirName, $catalog)
857
    {
858
        $file = str_replace('\\', '/', "$dirName/$catalog.pot");
859
        FileHelper::createDirectory(dirname($file));
860
        $this->stdout("Saving messages to $file...\n");
861
862
        $poFile = new GettextPoFile();
863
864
        $merged = [];
865
866
        $hasSomethingToWrite = false;
867
        foreach ($messages as $category => $msgs) {
868
            $msgs = array_values(array_unique($msgs));
869
870
            sort($msgs);
871
            foreach ($msgs as $message) {
872
                $merged[$category . chr(4) . $message] = '';
873
            }
874
            $this->stdout("Category \"$category\" merged.\n");
875
            $hasSomethingToWrite = true;
876
        }
877
        if ($hasSomethingToWrite) {
878
            ksort($merged);
879
            $poFile->save($file, $merged);
880
            $this->stdout("Translation saved.\n", Console::FG_GREEN);
881
        } else {
882
            $this->stdout("Nothing to save.\n", Console::FG_GREEN);
883
        }
884
    }
885
886
    /**
887
     * @param string $configFile
888
     * @throws Exception If configuration file does not exists.
889
     * @since 2.0.13
890
     */
891 45
    protected function initConfig($configFile)
892
    {
893 45
        $configFileContent = [];
894 45
        if ($configFile !== null) {
895 45
            $configFile = Yii::getAlias($configFile);
896 45
            if (!is_file($configFile)) {
897 3
                throw new Exception("The configuration file does not exist: $configFile");
898
            }
899 42
            $configFileContent = require $configFile;
900
        }
901
902 42
        $this->config = array_merge(
903 42
            $this->getOptionValues($this->action->id),
904 42
            $configFileContent,
905 42
            $this->getPassedOptionValues()
906
        );
907 42
        $this->config['sourcePath'] = Yii::getAlias($this->config['sourcePath']);
908 42
        $this->config['messagePath'] = Yii::getAlias($this->config['messagePath']);
909
910 42
        if (!isset($this->config['sourcePath'], $this->config['languages'])) {
911
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
912
        }
913 42
        if (!is_dir($this->config['sourcePath'])) {
914
            throw new Exception("The source path {$this->config['sourcePath']} is not a valid directory.");
915
        }
916 42
        if (empty($this->config['format']) || !in_array($this->config['format'], ['php', 'po', 'pot', 'db'])) {
917
            throw new Exception('Format should be either "php", "po", "pot" or "db".');
918
        }
919 42
        if (in_array($this->config['format'], ['php', 'po', 'pot'])) {
920 29
            if (!isset($this->config['messagePath'])) {
921
                throw new Exception('The configuration file must specify "messagePath".');
922
            }
923 29
            if (!is_dir($this->config['messagePath'])) {
924
                throw new Exception("The message path {$this->config['messagePath']} is not a valid directory.");
925
            }
926
        }
927 42
        if (empty($this->config['languages'])) {
928
            throw new Exception('Languages cannot be empty.');
929
        }
930
931 42
        if ($this->config['format'] === 'php' && $this->config['phpDocBlock'] === null) {
932
            $this->config['phpDocBlock'] = <<<DOCBLOCK
933
/**
934
 * Message translations.
935
 *
936
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
937
 * It contains the localizable messages extracted from source code.
938
 * You may modify this file by translating the extracted messages.
939
 *
940
 * Each array element represents the translation (value) of a message (key).
941
 * If the value is empty, the message is considered as not translated.
942
 * Messages that no longer need translation will have their translations
943
 * enclosed between a pair of '@@' marks.
944
 *
945
 * Message string can be used with plural forms format. Check i18n section
946
 * of the guide for details.
947
 *
948
 * NOTE: this file must be saved in UTF-8 encoding.
949
 */
950
DOCBLOCK;
951
        }
952 42
    }
953
}
954