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 ( c73de3...97c43c )
by Robert
13:22
created

MessageController::saveMessagesToDb()   F

Complexity

Conditions 24
Paths 3072

Size

Total Lines 102
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 86
CRAP Score 24.0008

Importance

Changes 0
Metric Value
dl 0
loc 102
rs 2
c 0
b 0
f 0
ccs 86
cts 87
cp 0.9885
cc 24
eloc 70
nc 3072
nop 7
crap 24.0008

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\db\Connection;
13
use yii\db\Query;
14
use yii\di\Instance;
15
use yii\helpers\Console;
16
use yii\helpers\FileHelper;
17
use yii\helpers\VarDumper;
18
use yii\i18n\GettextPoFile;
19
20
/**
21
 * Extracts messages to be translated from source files.
22
 *
23
 * The extracted messages can be saved the following depending on `format`
24
 * setting in config file:
25
 *
26
 * - PHP message source files.
27
 * - ".po" files.
28
 * - Database.
29
 *
30
 * Usage:
31
 * 1. Create a configuration file using the 'message/config' command:
32
 *    yii message/config /path/to/myapp/messages/config.php
33
 * 2. Edit the created config file, adjusting it for your web application needs.
34
 * 3. Run the 'message/extract' command, using created config:
35
 *    yii message /path/to/myapp/messages/config.php
36
 *
37
 * @author Qiang Xue <[email protected]>
38
 * @since 2.0
39
 */
40
class MessageController extends \yii\console\Controller
41
{
42
    /**
43
     * @var string controller default action ID.
44
     */
45
    public $defaultAction = 'extract';
46
    /**
47
     * @var string required, root directory of all source files.
48
     */
49
    public $sourcePath = '@yii';
50
    /**
51
     * @var string required, root directory containing message translations.
52
     */
53
    public $messagePath = '@yii/messages';
54
    /**
55
     * @var array required, list of language codes that the extracted messages
56
     * should be translated to. For example, ['zh-CN', 'de'].
57
     */
58
    public $languages = [];
59
    /**
60
     * @var string the name of the function for translating messages.
61
     * Defaults to 'Yii::t'. This is used as a mark to find the messages to be
62
     * translated. You may use a string for single function name or an array for
63
     * multiple function names.
64
     */
65
    public $translator = 'Yii::t';
66
    /**
67
     * @var bool whether to sort messages by keys when merging new messages
68
     * with the existing ones. Defaults to false, which means the new (untranslated)
69
     * messages will be separated from the old (translated) ones.
70
     */
71
    public $sort = false;
72
    /**
73
     * @var bool whether the message file should be overwritten with the merged messages
74
     */
75
    public $overwrite = true;
76
    /**
77
     * @var bool whether to remove messages that no longer appear in the source code.
78
     * Defaults to false, which means these messages will NOT be removed.
79
     */
80
    public $removeUnused = false;
81
    /**
82
     * @var bool whether to mark messages that no longer appear in the source code.
83
     * Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks.
84
     */
85
    public $markUnused = true;
86
    /**
87
     * @var array list of patterns that specify which files/directories should NOT be processed.
88
     * If empty or not set, all files/directories will be processed.
89
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
90
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
91
     */
92
    public $except = [
93
        '.svn',
94
        '.git',
95
        '.gitignore',
96
        '.gitkeep',
97
        '.hgignore',
98
        '.hgkeep',
99
        '/messages',
100
        '/BaseYii.php', // contains examples about Yii:t()
101
    ];
102
    /**
103
     * @var array list of patterns that specify which files (not directories) should be processed.
104
     * If empty or not set, all files will be processed.
105
     * See helpers/FileHelper::findFiles() description for pattern matching rules.
106
     * If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
107
     */
108
    public $only = ['*.php'];
109
    /**
110
     * @var string generated file format. Can be "php", "db", "po" or "pot".
111
     */
112
    public $format = 'php';
113
    /**
114
     * @var string connection component ID for "db" format.
115
     */
116
    public $db = 'db';
117
    /**
118
     * @var string custom name for source message table for "db" format.
119
     */
120
    public $sourceMessageTable = '{{%source_message}}';
121
    /**
122
     * @var string custom name for translation message table for "db" format.
123
     */
124
    public $messageTable = '{{%message}}';
125
    /**
126
     * @var string name of the file that will be used for translations for "po" format.
127
     */
128
    public $catalog = 'messages';
129
    /**
130
     * @var array message categories to ignore. For example, 'yii', 'app*', 'widgets/menu', etc.
131
     * @see isCategoryIgnored
132
     */
133
    public $ignoreCategories = [];
134
135
136
    /**
137
     * @inheritdoc
138
     */
139 35
    public function options($actionID)
140
    {
141 35
        return array_merge(parent::options($actionID), [
142 35
            'sourcePath',
143 35
            'messagePath',
144 35
            'languages',
145 35
            'translator',
146 35
            'sort',
147 35
            'overwrite',
148 35
            'removeUnused',
149 35
            'markUnused',
150 35
            'except',
151 35
            'only',
152 35
            'format',
153 35
            'db',
154 35
            'sourceMessageTable',
155 35
            'messageTable',
156 35
            'catalog',
157 35
            'ignoreCategories',
158 35
        ]);
159
    }
160
161
    /**
162
     * @inheritdoc
163
     * @since 2.0.8
164
     */
165
    public function optionAliases()
166
    {
167
        return array_merge(parent::optionAliases(), [
168
            'c' => 'catalog',
169
            'e' => 'except',
170
            'f' => 'format',
171
            'i' => 'ignoreCategories',
172
            'l' => 'languages',
173
            'u' => 'markUnused',
174
            'p' => 'messagePath',
175
            'o' => 'only',
176
            'w' => 'overwrite',
177
            'S' => 'sort',
178
            't' => 'translator',
179
            'm' => 'sourceMessageTable',
180
            's' => 'sourcePath',
181
            'r' => 'removeUnused',
182
        ]);
183
    }
184
185
    /**
186
     * Creates a configuration file for the "extract" command using command line options specified
187
     *
188
     * The generated configuration file contains parameters required
189
     * for source code messages extraction.
190
     * You may use this configuration file with the "extract" command.
191
     *
192
     * @param string $filePath output file name or alias.
193
     * @return int CLI exit code
194
     * @throws Exception on failure.
195
     */
196 3
    public function actionConfig($filePath)
197
    {
198 3
        $filePath = Yii::getAlias($filePath);
199 3
        if (file_exists($filePath)) {
200
            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...
201
                return self::EXIT_CODE_NORMAL;
202
            }
203
        }
204
205 3
        $array = VarDumper::export($this->getOptionValues($this->action->id));
206
        $content = <<<EOD
207
<?php
208
/**
209 3
 * Configuration file for 'yii {$this->id}/{$this->defaultAction}' command.
210
 *
211 3
 * This file is automatically generated by 'yii {$this->id}/{$this->action->id}' command.
212
 * It contains parameters for source code messages extraction.
213
 * You may modify this file to suit your needs.
214
 *
215 3
 * You can use 'yii {$this->id}/{$this->action->id}-template' command to create
216
 * template configuration file with detailed description for each parameter.
217
 */
218 3
return $array;
219
220 3
EOD;
221
222 3
        if (file_put_contents($filePath, $content) === false) {
223
            $this->stdout("Configuration file was NOT created: '{$filePath}'.\n\n", Console::FG_RED);
224
            return self::EXIT_CODE_ERROR;
225
        }
226
227 3
        $this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN);
228 3
        return self::EXIT_CODE_NORMAL;
229
    }
230
231
    /**
232
     * Creates a configuration file template for the "extract" command.
233
     *
234
     * The created configuration file contains detailed instructions on
235
     * how to customize it to fit for your needs. After customization,
236
     * you may use this configuration file with the "extract" command.
237
     *
238
     * @param string $filePath output file name or alias.
239
     * @return int CLI exit code
240
     * @throws Exception on failure.
241
     */
242
    public function actionConfigTemplate($filePath)
243
    {
244
        $filePath = Yii::getAlias($filePath);
245
246
        if (file_exists($filePath)) {
247
            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...
248
                return self::EXIT_CODE_NORMAL;
249
            }
250
        }
251
252
        if (!copy(Yii::getAlias('@yii/views/messageConfig.php'), $filePath)) {
253
            $this->stdout("Configuration file template was NOT created at '{$filePath}'.\n\n", Console::FG_RED);
254
            return self::EXIT_CODE_ERROR;
255
        }
256
257
        $this->stdout("Configuration file template created at '{$filePath}'.\n\n", Console::FG_GREEN);
258
        return self::EXIT_CODE_NORMAL;
259
    }
260
261
    /**
262
     * Extracts messages to be translated from source code.
263
     *
264
     * This command will search through source code files and extract
265
     * messages that need to be translated in different languages.
266
     *
267
     * @param string $configFile the path or alias of the configuration file.
268
     * You may use the "yii message/config" command to generate
269
     * this file and then customize it for your needs.
270
     * @throws Exception on failure.
271
     */
272 32
    public function actionExtract($configFile = null)
273
    {
274 32
        $configFileContent = [];
275 32
        if ($configFile !== null) {
276 32
            $configFile = Yii::getAlias($configFile);
277 32
            if (!is_file($configFile)) {
278 3
                throw new Exception("The configuration file does not exist: $configFile");
279
            }
280 29
            $configFileContent = require($configFile);
281 29
        }
282
283 29
        $config = array_merge(
284 29
            $this->getOptionValues($this->action->id),
285 29
            $configFileContent,
286 29
            $this->getPassedOptionValues()
287 29
        );
288 29
        $config['sourcePath'] = Yii::getAlias($config['sourcePath']);
289 29
        $config['messagePath'] = Yii::getAlias($config['messagePath']);
290
291 29
        if (!isset($config['sourcePath'], $config['languages'])) {
292
            throw new Exception('The configuration file must specify "sourcePath" and "languages".');
293
        }
294 29
        if (!is_dir($config['sourcePath'])) {
295
            throw new Exception("The source path {$config['sourcePath']} is not a valid directory.");
296
        }
297 29
        if (empty($config['format']) || !in_array($config['format'], ['php', 'po', 'pot', 'db'])) {
298
            throw new Exception('Format should be either "php", "po", "pot" or "db".');
299
        }
300 29
        if (in_array($config['format'], ['php', 'po', 'pot'])) {
301 20
            if (!isset($config['messagePath'])) {
302
                throw new Exception('The configuration file must specify "messagePath".');
303
            }
304 20
            if (!is_dir($config['messagePath'])) {
305
                throw new Exception("The message path {$config['messagePath']} is not a valid directory.");
306
            }
307 20
        }
308 29
        if (empty($config['languages'])) {
309
            throw new Exception('Languages cannot be empty.');
310
        }
311
312 29
        $files = FileHelper::findFiles(realpath($config['sourcePath']), $config);
313
314 29
        $messages = [];
315 29
        foreach ($files as $file) {
316 29
            $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'], $config['ignoreCategories']));
317 29
        }
318
319 29
        $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages';
320
321 29
        if (in_array($config['format'], ['php', 'po'])) {
322 20
            foreach ($config['languages'] as $language) {
323 20
                $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language;
324 20
                if (!is_dir($dir) && !@mkdir($dir)) {
325
                    throw new Exception("Directory '{$dir}' can not be created.");
326
                }
327 20
                if ($config['format'] === 'po') {
328 10
                    $this->saveMessagesToPO($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $catalog, $config['markUnused']);
329 10
                } else {
330 10
                    $this->saveMessagesToPHP($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['markUnused']);
331
                }
332 20
            }
333 29
        } elseif ($config['format'] === 'db') {
334
            /** @var Connection $db */
335 9
            $db = Instance::ensure($config['db'], Connection::className());
336 9
            $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}';
337 9
            $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}';
338 9
            $this->saveMessagesToDb(
339 9
                $messages,
340 9
                $db,
341 9
                $sourceMessageTable,
342 9
                $messageTable,
343 9
                $config['removeUnused'],
344 9
                $config['languages'],
345 9
                $config['markUnused']
346 9
            );
347 9
        } elseif ($config['format'] === 'pot') {
348
            $this->saveMessagesToPOT($messages, $config['messagePath'], $catalog);
0 ignored issues
show
Bug introduced by
It seems like $config['messagePath'] can also be of type boolean; however, yii\console\controllers\...er::saveMessagesToPOT() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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