Complex classes like MessageController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MessageController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 | 26 | public function options($actionID) |
|
140 | { |
||
141 | 26 | return array_merge(parent::options($actionID), [ |
|
142 | 26 | 'sourcePath', |
|
143 | 'messagePath', |
||
144 | 'languages', |
||
145 | 'translator', |
||
146 | 'sort', |
||
147 | 'overwrite', |
||
148 | 'removeUnused', |
||
149 | 'markUnused', |
||
150 | 'except', |
||
151 | 'only', |
||
152 | 'format', |
||
153 | 'db', |
||
154 | 'sourceMessageTable', |
||
155 | 'messageTable', |
||
156 | 'catalog', |
||
157 | 'ignoreCategories', |
||
158 | ]); |
||
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 | 2 | public function actionConfig($filePath) |
|
197 | { |
||
198 | 2 | $filePath = Yii::getAlias($filePath); |
|
199 | 2 | if (file_exists($filePath)) { |
|
200 | if (!$this->confirm("File '{$filePath}' already exists. Do you wish to overwrite it?")) { |
||
|
|||
201 | return self::EXIT_CODE_NORMAL; |
||
202 | } |
||
203 | } |
||
204 | |||
205 | 2 | $array = VarDumper::export($this->getOptionValues($this->action->id)); |
|
206 | $content = <<<EOD |
||
207 | <?php |
||
208 | /** |
||
209 | 2 | * Configuration file for 'yii {$this->id}/{$this->defaultAction}' command. |
|
210 | * |
||
211 | 2 | * 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 | 2 | * 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 | 2 | return $array; |
|
219 | |||
220 | EOD; |
||
221 | |||
222 | 2 | 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 | 2 | $this->stdout("Configuration file created: '{$filePath}'.\n\n", Console::FG_GREEN); |
|
228 | 2 | 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) |
||
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 | 24 | public function actionExtract($configFile = null) |
|
273 | { |
||
274 | 24 | $configFileContent = []; |
|
275 | 24 | if ($configFile !== null) { |
|
276 | 24 | $configFile = Yii::getAlias($configFile); |
|
277 | 24 | if (!is_file($configFile)) { |
|
278 | 2 | throw new Exception("The configuration file does not exist: $configFile"); |
|
279 | } |
||
280 | 22 | $configFileContent = require($configFile); |
|
281 | } |
||
282 | |||
283 | 22 | $config = array_merge( |
|
284 | 22 | $this->getOptionValues($this->action->id), |
|
285 | 22 | $configFileContent, |
|
286 | 22 | $this->getPassedOptionValues() |
|
287 | ); |
||
288 | 22 | $config['sourcePath'] = Yii::getAlias($config['sourcePath']); |
|
289 | 22 | $config['messagePath'] = Yii::getAlias($config['messagePath']); |
|
290 | |||
291 | 22 | if (!isset($config['sourcePath'], $config['languages'])) { |
|
292 | throw new Exception('The configuration file must specify "sourcePath" and "languages".'); |
||
293 | } |
||
294 | 22 | if (!is_dir($config['sourcePath'])) { |
|
295 | throw new Exception("The source path {$config['sourcePath']} is not a valid directory."); |
||
296 | } |
||
297 | 22 | 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 | 22 | if (in_array($config['format'], ['php', 'po', 'pot'])) { |
|
301 | 22 | if (!isset($config['messagePath'])) { |
|
302 | throw new Exception('The configuration file must specify "messagePath".'); |
||
303 | } |
||
304 | 22 | if (!is_dir($config['messagePath'])) { |
|
305 | throw new Exception("The message path {$config['messagePath']} is not a valid directory."); |
||
306 | } |
||
307 | } |
||
308 | 22 | if (empty($config['languages'])) { |
|
309 | throw new Exception('Languages cannot be empty.'); |
||
310 | } |
||
311 | |||
312 | 22 | $files = FileHelper::findFiles(realpath($config['sourcePath']), $config); |
|
313 | |||
314 | 22 | $messages = []; |
|
315 | 22 | foreach ($files as $file) { |
|
316 | 22 | $messages = array_merge_recursive($messages, $this->extractMessages($file, $config['translator'], $config['ignoreCategories'])); |
|
317 | } |
||
318 | |||
319 | 22 | $catalog = isset($config['catalog']) ? $config['catalog'] : 'messages'; |
|
320 | |||
321 | 22 | if (in_array($config['format'], ['php', 'po'])) { |
|
322 | 22 | foreach ($config['languages'] as $language) { |
|
323 | 22 | $dir = $config['messagePath'] . DIRECTORY_SEPARATOR . $language; |
|
324 | 22 | if (!is_dir($dir) && !@mkdir($dir)) { |
|
325 | throw new Exception("Directory '{$dir}' can not be created."); |
||
326 | } |
||
327 | 22 | if ($config['format'] === 'po') { |
|
328 | 11 | $this->saveMessagesToPO($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $catalog, $config['markUnused']); |
|
329 | } else { |
||
330 | 22 | $this->saveMessagesToPHP($messages, $dir, $config['overwrite'], $config['removeUnused'], $config['sort'], $config['markUnused']); |
|
331 | } |
||
332 | } |
||
333 | } elseif ($config['format'] === 'db') { |
||
334 | /** @var Connection $db */ |
||
335 | $db = Instance::ensure($config['db'], Connection::className()); |
||
336 | $sourceMessageTable = isset($config['sourceMessageTable']) ? $config['sourceMessageTable'] : '{{%source_message}}'; |
||
337 | $messageTable = isset($config['messageTable']) ? $config['messageTable'] : '{{%message}}'; |
||
338 | $this->saveMessagesToDb( |
||
339 | $messages, |
||
340 | $db, |
||
341 | $sourceMessageTable, |
||
342 | $messageTable, |
||
343 | $config['removeUnused'], |
||
344 | $config['languages'], |
||
345 | $config['markUnused'] |
||
346 | ); |
||
347 | } elseif ($config['format'] === 'pot') { |
||
348 | $this->saveMessagesToPOT($messages, $config['messagePath'], $catalog); |
||
349 | } |
||
350 | 22 | } |
|
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 | protected function saveMessagesToDb($messages, $db, $sourceMessageTable, $messageTable, $removeUnused, $languages, $markUnused) |
||
364 | { |
||
365 | $currentMessages = []; |
||
366 | $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db); |
||
367 | foreach ($rows as $row) { |
||
368 | $currentMessages[$row['category']][$row['id']] = $row['message']; |
||
369 | } |
||
370 | |||
371 | $currentLanguages = []; |
||
372 | $rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db); |
||
373 | foreach ($rows as $row) { |
||
374 | $currentLanguages[] = $row['language']; |
||
375 | } |
||
376 | $missingLanguages = []; |
||
377 | if (!empty($currentLanguages)) { |
||
378 | $missingLanguages = array_diff($languages, $currentLanguages); |
||
379 | } |
||
380 | |||
381 | $new = []; |
||
382 | $obsolete = []; |
||
383 | |||
384 | foreach ($messages as $category => $msgs) { |
||
385 | $msgs = array_unique($msgs); |
||
386 | |||
387 | if (isset($currentMessages[$category])) { |
||
388 | $new[$category] = array_diff($msgs, $currentMessages[$category]); |
||
389 | $obsolete += array_diff($currentMessages[$category], $msgs); |
||
390 | } else { |
||
391 | $new[$category] = $msgs; |
||
392 | } |
||
393 | } |
||
394 | |||
395 | foreach (array_diff(array_keys($currentMessages), array_keys($messages)) as $category) { |
||
396 | $obsolete += $currentMessages[$category]; |
||
397 | } |
||
398 | |||
399 | if (!$removeUnused) { |
||
400 | foreach ($obsolete as $pk => $msg) { |
||
401 | if (mb_substr($msg, 0, 2) === '@@' && mb_substr($msg, -2) === '@@') { |
||
402 | unset($obsolete[$pk]); |
||
403 | } |
||
404 | } |
||
405 | } |
||
406 | |||
407 | $obsolete = array_keys($obsolete); |
||
408 | $this->stdout('Inserting new messages...'); |
||
409 | $savedFlag = false; |
||
410 | |||
411 | foreach ($new as $category => $msgs) { |
||
412 | foreach ($msgs as $msg) { |
||
413 | $savedFlag = true; |
||
414 | $lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]); |
||
415 | foreach ($languages as $language) { |
||
416 | $db->createCommand() |
||
417 | ->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language]) |
||
418 | ->execute(); |
||
419 | } |
||
420 | } |
||
421 | } |
||
422 | |||
423 | if (!empty($missingLanguages)) { |
||
424 | $updatedMessages = []; |
||
425 | $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db); |
||
426 | foreach ($rows as $row) { |
||
427 | $updatedMessages[$row['category']][$row['id']] = $row['message']; |
||
428 | } |
||
429 | foreach ($updatedMessages as $category => $msgs) { |
||
430 | foreach ($msgs as $id => $msg) { |
||
431 | $savedFlag = true; |
||
432 | foreach ($missingLanguages as $language) { |
||
433 | $db->createCommand() |
||
434 | ->insert($messageTable, ['id' => $id, 'language' => $language]) |
||
435 | ->execute(); |
||
436 | } |
||
437 | } |
||
438 | } |
||
439 | } |
||
440 | |||
441 | $this->stdout($savedFlag ? "saved.\n" : "Nothing to save.\n"); |
||
442 | $this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...'); |
||
443 | |||
444 | if (empty($obsolete)) { |
||
445 | $this->stdout("Nothing obsoleted...skipped.\n"); |
||
446 | return; |
||
447 | } |
||
448 | |||
449 | if ($removeUnused) { |
||
450 | $db->createCommand() |
||
451 | ->delete($sourceMessageTable, ['in', 'id', $obsolete]) |
||
452 | ->execute(); |
||
453 | $this->stdout("deleted.\n"); |
||
454 | } elseif ($markUnused) { |
||
455 | $rows = (new Query()) |
||
456 | ->select(['id', 'message']) |
||
457 | ->from($sourceMessageTable) |
||
458 | ->where(['in', 'id', $obsolete]) |
||
459 | ->all($db); |
||
460 | |||
461 | foreach ($rows as $row) { |
||
462 | $db->createCommand()->update( |
||
463 | $sourceMessageTable, |
||
464 | ['message' => '@@' . $row['message'] . '@@'], |
||
465 | ['id' => $row['id']] |
||
466 | )->execute(); |
||
467 | } |
||
468 | $this->stdout("updated.\n"); |
||
469 | } else { |
||
470 | $this->stdout("kept untouched.\n"); |
||
471 | } |
||
472 | } |
||
473 | |||
474 | /** |
||
475 | * Extracts messages from a file |
||
476 | * |
||
477 | * @param string $fileName name of the file to extract messages from |
||
478 | * @param string $translator name of the function used to translate messages |
||
479 | * @param array $ignoreCategories message categories to ignore. |
||
480 | * This parameter is available since version 2.0.4. |
||
481 | * @return array |
||
482 | */ |
||
483 | 22 | protected function extractMessages($fileName, $translator, $ignoreCategories = []) |
|
502 | |||
503 | /** |
||
504 | * Extracts messages from a parsed PHP tokens list. |
||
505 | * @param array $tokens tokens to be processed. |
||
506 | * @param array $translatorTokens translator tokens. |
||
507 | * @param array $ignoreCategories message categories to ignore. |
||
508 | * @return array messages. |
||
509 | */ |
||
510 | 22 | protected function extractMessagesFromTokens(array $tokens, array $translatorTokens, array $ignoreCategories) |
|
587 | |||
588 | /** |
||
589 | * The method checks, whether the $category is ignored according to $ignoreCategories array. |
||
590 | * Examples: |
||
591 | * |
||
592 | * - `myapp` - will be ignored only `myapp` category; |
||
593 | * - `myapp*` - will be ignored by all categories beginning with `myapp` (`myapp`, `myapplication`, `myapprove`, `myapp/widgets`, `myapp.widgets`, etc). |
||
594 | * |
||
595 | * @param string $category category that is checked |
||
596 | * @param array $ignoreCategories message categories to ignore. |
||
597 | * @return bool |
||
598 | * @since 2.0.7 |
||
599 | */ |
||
600 | 22 | protected function isCategoryIgnored($category, array $ignoreCategories) |
|
615 | |||
616 | /** |
||
617 | * Finds out if two PHP tokens are equal |
||
618 | * |
||
619 | * @param array|string $a |
||
620 | * @param array|string $b |
||
621 | * @return bool |
||
622 | * @since 2.0.1 |
||
623 | */ |
||
624 | 22 | protected function tokensEqual($a, $b) |
|
634 | |||
635 | /** |
||
636 | * Finds out a line of the first non-char PHP token found |
||
637 | * |
||
638 | * @param array $tokens |
||
639 | * @return int|string |
||
640 | * @since 2.0.1 |
||
641 | */ |
||
642 | protected function getLine($tokens) |
||
651 | |||
652 | /** |
||
653 | * Writes messages into PHP files |
||
654 | * |
||
655 | * @param array $messages |
||
656 | * @param string $dirName name of the directory to write to |
||
657 | * @param bool $overwrite if existing file should be overwritten without backup |
||
658 | * @param bool $removeUnused if obsolete translations should be removed |
||
659 | * @param bool $sort if translations should be sorted |
||
660 | * @param bool $markUnused if obsolete translations should be marked |
||
661 | */ |
||
662 | 11 | protected function saveMessagesToPHP($messages, $dirName, $overwrite, $removeUnused, $sort, $markUnused) |
|
674 | |||
675 | /** |
||
676 | * Writes category messages into PHP file |
||
677 | * |
||
678 | * @param array $messages |
||
679 | * @param string $fileName name of the file to write to |
||
680 | * @param bool $overwrite if existing file should be overwritten without backup |
||
681 | * @param bool $removeUnused if obsolete translations should be removed |
||
682 | * @param bool $sort if translations should be sorted |
||
683 | * @param string $category message category |
||
684 | * @param bool $markUnused if obsolete translations should be marked |
||
685 | * @return int exit code |
||
686 | */ |
||
687 | 11 | protected function saveMessagesCategoryToPHP($messages, $fileName, $overwrite, $removeUnused, $sort, $category, $markUnused) |
|
772 | |||
773 | /** |
||
774 | * Writes messages into PO file |
||
775 | * |
||
776 | * @param array $messages |
||
777 | * @param string $dirName name of the directory to write to |
||
778 | * @param bool $overwrite if existing file should be overwritten without backup |
||
779 | * @param bool $removeUnused if obsolete translations should be removed |
||
780 | * @param bool $sort if translations should be sorted |
||
781 | * @param string $catalog message catalog |
||
782 | * @param bool $markUnused if obsolete translations should be marked |
||
783 | */ |
||
784 | 11 | protected function saveMessagesToPO($messages, $dirName, $overwrite, $removeUnused, $sort, $catalog, $markUnused) |
|
868 | |||
869 | /** |
||
870 | * Writes messages into POT file |
||
871 | * |
||
872 | * @param array $messages |
||
873 | * @param string $dirName name of the directory to write to |
||
874 | * @param string $catalog message catalog |
||
875 | * @since 2.0.6 |
||
876 | */ |
||
877 | protected function saveMessagesToPOT($messages, $dirName, $catalog) |
||
906 | } |
||
907 |
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.