HelpController   F
last analyzed

Complexity

Total Complexity 95

Size/Duplication

Total Lines 523
Duplicated Lines 0 %

Test Coverage

Coverage 83.7%

Importance

Changes 0
Metric Value
eloc 258
dl 0
loc 523
ccs 226
cts 270
cp 0.837
rs 2
c 0
b 0
f 0
wmc 95

17 Methods

Rating   Name   Duplication   Size   Complexity  
A validateControllerClass() 0 8 3
A getActions() 0 13 6
B actionUsage() 0 31 7
A getScriptName() 0 3 1
A getCommandDescriptions() 0 11 2
A getDefaultHelpHeader() 0 3 1
A actionList() 0 13 4
C formatOptionHelp() 0 35 11
A actionIndex() 0 19 6
A camel2id() 0 3 1
A getCommands() 0 12 3
A formatOptionAliases() 0 9 3
B actionListActionOptions() 0 24 8
B getDefaultHelp() 0 54 9
B getCommandHelp() 0 37 8
C getSubCommandHelp() 0 70 12
B getModuleCommands() 0 43 10

How to fix   Complexity   

Complex Class

Complex classes like HelpController 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.

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 HelpController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\console\controllers;
10
11
use Yii;
12
use yii\base\Application;
13
use yii\console\Controller;
14
use yii\console\Exception;
15
use yii\helpers\Console;
16
use yii\helpers\Inflector;
17
18
/**
19
 * Provides help information about console commands.
20
 *
21
 * This command displays the available command list in
22
 * the application or the detailed instructions about using
23
 * a specific command.
24
 *
25
 * This command can be used as follows on command line:
26
 *
27
 * ```
28
 * yii help [command name]
29
 * ```
30
 *
31
 * In the above, if the command name is not provided, all
32
 * available commands will be displayed.
33
 *
34
 * @property-read array $commands All available command names.
35
 *
36
 * @author Qiang Xue <[email protected]>
37
 * @since 2.0
38
 */
39
class HelpController extends Controller
40
{
41
    /**
42
     * Displays available commands or the detailed information
43
     * about a particular command.
44
     *
45
     * @param string|null $command The name of the command to show help about.
46
     * If not provided, all available commands will be displayed.
47
     * @return int the exit status
48
     * @throws Exception if the command for help is unknown
49
     */
50 5
    public function actionIndex($command = null)
51
    {
52 5
        if ($command !== null) {
53 3
            $result = Yii::$app->createController($command);
54 3
            if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
55
                $name = $this->ansiFormat($command, Console::FG_YELLOW);
56
                throw new Exception("No help for unknown command \"$name\".");
57
            }
58
59 3
            list($controller, $actionID) = $result;
60
61 3
            $actions = $this->getActions($controller);
62 3
            if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
63 3
                $this->getSubCommandHelp($controller, $actionID);
64
            } else {
65 3
                $this->getCommandHelp($controller);
66
            }
67
        } else {
68 2
            $this->getDefaultHelp();
69
        }
70
    }
71
72
    /**
73
     * List all available controllers and actions in machine readable format.
74
     * This is used for shell completion.
75
     * @since 2.0.11
76
     */
77 3
    public function actionList()
78
    {
79 3
        foreach ($this->getCommandDescriptions() as $command => $description) {
80 3
            $result = Yii::$app->createController($command);
81
            /** @var $controller Controller */
82 3
            list($controller, $actionID) = $result;
83 3
            $actions = $this->getActions($controller);
84 3
            $prefix = $controller->getUniqueId();
85 3
            if ($controller->createAction($controller->defaultAction) !== null) {
86 3
                $this->stdout("$prefix\n");
87
            }
88 3
            foreach ($actions as $action) {
89 3
                $this->stdout("$prefix/$action\n");
90
            }
91
        }
92
    }
93
94
    /**
95
     * List all available options for the $action in machine readable format.
96
     * This is used for shell completion.
97
     *
98
     * @param string $action route to action
99
     * @since 2.0.11
100
     */
101 1
    public function actionListActionOptions($action)
102
    {
103 1
        $result = Yii::$app->createController($action);
104
105 1
        if ($result === false || !($result[0] instanceof Controller)) {
106
            return;
107
        }
108
109
        /** @var Controller $controller */
110 1
        list($controller, $actionID) = $result;
111 1
        $action = $controller->createAction($actionID);
112 1
        if ($action === null) {
113
            return;
114
        }
115
116 1
        foreach ($controller->getActionArgsHelp($action) as $argument => $help) {
117 1
            $description = preg_replace('~\R~', '', addcslashes($help['comment'], ':')) ?: $argument;
118 1
            $this->stdout($argument . ':' . $description . "\n");
119
        }
120
121 1
        $this->stdout("\n");
122 1
        foreach ($controller->getActionOptionsHelp($action) as $argument => $help) {
123 1
            $description = preg_replace('~\R~', '', addcslashes($help['comment'], ':'));
124 1
            $this->stdout('--' . $argument . ($description ? ':' . $description : '') . "\n");
125
        }
126
    }
127
128
    /**
129
     * Displays usage information for $action.
130
     *
131
     * @param string $action route to action
132
     * @since 2.0.11
133
     */
134 1
    public function actionUsage($action)
135
    {
136 1
        $result = Yii::$app->createController($action);
137
138 1
        if ($result === false || !($result[0] instanceof Controller)) {
139
            return;
140
        }
141
142
        /** @var Controller $controller */
143 1
        list($controller, $actionID) = $result;
144 1
        $action = $controller->createAction($actionID);
145 1
        if ($action === null) {
146
            return;
147
        }
148
149 1
        $scriptName = $this->getScriptName();
150 1
        if ($action->id === $controller->defaultAction) {
151
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
152
        } else {
153 1
            $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
154
        }
155
156 1
        foreach ($controller->getActionArgsHelp($action) as $name => $arg) {
157 1
            if ($arg['required']) {
158 1
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
159
            } else {
160
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
161
            }
162
        }
163
164 1
        $this->stdout("\n");
165
    }
166
167
    /**
168
     * Returns all available command names.
169
     * @return array all available command names
170
     */
171 21
    public function getCommands()
172
    {
173 21
        $commands = $this->getModuleCommands(Yii::$app);
174 21
        sort($commands);
175 21
        return array_filter(array_unique($commands), function ($command) {
176 21
            $result = Yii::$app->createController($command);
177 21
            if ($result === false || !$result[0] instanceof Controller) {
178
                return false;
179
            }
180 21
            list($controller, $actionID) = $result;
181 21
            $actions = $this->getActions($controller);
182 21
            return $actions !== [];
183 21
        });
184
    }
185
186
    /**
187
     * Returns an array of commands an their descriptions.
188
     * @return array all available commands as keys and their description as values.
189
     */
190 5
    protected function getCommandDescriptions()
191
    {
192 5
        $descriptions = [];
193 5
        foreach ($this->getCommands() as $command) {
194 5
            $result = Yii::$app->createController($command);
195
            /** @var Controller $controller */
196 5
            list($controller, $actionID) = $result;
197 5
            $descriptions[$command] = $controller->getHelpSummary();
198
        }
199
200 5
        return $descriptions;
201
    }
202
203
    /**
204
     * Returns all available actions of the specified controller.
205
     * @param Controller $controller the controller instance
206
     * @return array all available action IDs.
207
     */
208 24
    public function getActions($controller)
209
    {
210 24
        $actions = array_keys($controller->actions());
211 24
        $class = new \ReflectionClass($controller);
212 24
        foreach ($class->getMethods() as $method) {
213 24
            $name = $method->getName();
214 24
            if ($name !== 'actions' && $method->isPublic() && !$method->isStatic() && strncmp($name, 'action', 6) === 0) {
215 24
                $actions[] = $this->camel2id(substr($name, 6));
216
            }
217
        }
218 24
        sort($actions);
219
220 24
        return array_unique($actions);
221
    }
222
223
    /**
224
     * Returns available commands of a specified module.
225
     * @param \yii\base\Module $module the module instance
226
     * @return array the available command names
227
     */
228 21
    protected function getModuleCommands($module)
229
    {
230 21
        $prefix = $module instanceof Application ? '' : $module->getUniqueId() . '/';
231
232 21
        $commands = [];
233 21
        foreach (array_keys($module->controllerMap) as $id) {
234 21
            $commands[] = $prefix . $id;
235
        }
236
237 21
        foreach ($module->getModules() as $id => $child) {
238 1
            if (($child = $module->getModule($id)) === null) {
239
                continue;
240
            }
241 1
            foreach ($this->getModuleCommands($child) as $command) {
242 1
                $commands[] = $command;
243
            }
244
        }
245
246 21
        $controllerPath = $module->getControllerPath();
247 21
        if (is_dir($controllerPath)) {
248 3
            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($controllerPath, \RecursiveDirectoryIterator::KEY_AS_PATHNAME));
249 3
            $iterator = new \RegexIterator($iterator, '/.*Controller\.php$/', \RecursiveRegexIterator::GET_MATCH);
250 3
            foreach ($iterator as $matches) {
251 3
                $file = $matches[0];
252 3
                $relativePath = str_replace($controllerPath, '', $file);
253 3
                $class = strtr($relativePath, [
254 3
                    '/' => '\\',
255 3
                    '.php' => '',
256 3
                ]);
257 3
                $controllerClass = $module->controllerNamespace . $class;
258 3
                if ($this->validateControllerClass($controllerClass)) {
259 3
                    $dir = ltrim(pathinfo($relativePath, PATHINFO_DIRNAME), '\\/');
0 ignored issues
show
Bug introduced by
It seems like pathinfo($relativePath, ...llers\PATHINFO_DIRNAME) can also be of type array; however, parameter $string of ltrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

259
                    $dir = ltrim(/** @scrutinizer ignore-type */ pathinfo($relativePath, PATHINFO_DIRNAME), '\\/');
Loading history...
260
261 3
                    $command = Inflector::camel2id(substr(basename($file), 0, -14), '-', true);
262 3
                    if (!empty($dir)) {
263 1
                        $command = $dir . '/' . $command;
264
                    }
265 3
                    $commands[] = $prefix . $command;
266
                }
267
            }
268
        }
269
270 21
        return $commands;
271
    }
272
273
    /**
274
     * Validates if the given class is a valid console controller class.
275
     * @param string $controllerClass
276
     * @return bool
277
     */
278 3
    protected function validateControllerClass($controllerClass)
279
    {
280 3
        if (class_exists($controllerClass)) {
281 3
            $class = new \ReflectionClass($controllerClass);
282 3
            return !$class->isAbstract() && $class->isSubclassOf('yii\console\Controller');
283
        }
284
285
        return false;
286
    }
287
288
    /**
289
     * Displays all available commands.
290
     */
291 2
    protected function getDefaultHelp()
292
    {
293 2
        $commands = $this->getCommandDescriptions();
294 2
        $this->stdout($this->getDefaultHelpHeader());
295 2
        if (empty($commands)) {
296
            $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
297
            return;
298
        }
299
300 2
        $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
301 2
        $maxLength = 0;
302 2
        foreach ($commands as $command => $description) {
303 2
            $result = Yii::$app->createController($command);
304
            /** @var $controller Controller */
305 2
            list($controller, $actionID) = $result;
306 2
            $actions = $this->getActions($controller);
307 2
            $prefix = $controller->getUniqueId();
308 2
            foreach ($actions as $action) {
309 2
                $string = $prefix . '/' . $action;
310 2
                if ($action === $controller->defaultAction) {
311 2
                    $string .= ' (default)';
312
                }
313 2
                $maxLength = max($maxLength, strlen($string));
314
            }
315
        }
316 2
        foreach ($commands as $command => $description) {
317 2
            $result = Yii::$app->createController($command);
318 2
            list($controller, $actionID) = $result;
319 2
            $actions = $this->getActions($controller);
320 2
            $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
321 2
            $this->stdout(str_repeat(' ', $maxLength + 4 - strlen($command)));
322 2
            $this->stdout(Console::wrapText($description, $maxLength + 4 + 2), Console::BOLD);
323 2
            $this->stdout("\n");
324 2
            $prefix = $controller->getUniqueId();
325 2
            foreach ($actions as $action) {
326 2
                $string = '  ' . $prefix . '/' . $action;
327 2
                $this->stdout('  ' . $this->ansiFormat($string, Console::FG_GREEN));
328 2
                if ($action === $controller->defaultAction) {
329 2
                    $string .= ' (default)';
330 2
                    $this->stdout(' (default)', Console::FG_YELLOW);
331
                }
332 2
                $summary = $controller->getActionHelpSummary($controller->createAction($action));
333 2
                if ($summary !== '') {
334 2
                    $this->stdout(str_repeat(' ', $maxLength + 4 - strlen($string)));
335 2
                    $this->stdout(Console::wrapText($summary, $maxLength + 4 + 2));
336
                }
337 2
                $this->stdout("\n");
338
            }
339 2
            $this->stdout("\n");
340
        }
341 2
        $scriptName = $this->getScriptName();
342 2
        $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
343 2
        $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
344 2
            . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
345
    }
346
347
    /**
348
     * Displays the overall information of the command.
349
     * @param Controller $controller the controller instance
350
     */
351
    protected function getCommandHelp($controller)
352
    {
353
        $controller->color = $this->color;
354
355
        $this->stdout("\nDESCRIPTION\n", Console::BOLD);
356
        $comment = $controller->getHelp();
357
        if ($comment !== '') {
358
            $this->stdout("\n$comment\n\n");
359
        }
360
361
        $actions = $this->getActions($controller);
362
        if (!empty($actions)) {
363
            $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
364
            $prefix = $controller->getUniqueId();
365
366
            $maxlen = 5;
367
            foreach ($actions as $action) {
368
                $len = strlen($prefix . '/' . $action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
369
                $maxlen = max($maxlen, $len);
370
            }
371
            foreach ($actions as $action) {
372
                $this->stdout('- ' . $this->ansiFormat($prefix . '/' . $action, Console::FG_YELLOW));
373
                $len = strlen($prefix . '/' . $action) + 2;
374
                if ($action === $controller->defaultAction) {
375
                    $this->stdout(' (default)', Console::FG_GREEN);
376
                    $len += 10;
377
                }
378
                $summary = $controller->getActionHelpSummary($controller->createAction($action));
379
                if ($summary !== '') {
380
                    $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
381
                }
382
                $this->stdout("\n");
383
            }
384
            $scriptName = $this->getScriptName();
385
            $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
386
            $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
387
                . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
388
        }
389
    }
390
391
    /**
392
     * Displays the detailed information of a command action.
393
     * @param Controller $controller the controller instance
394
     * @param string $actionID action ID
395
     * @throws Exception if the action does not exist
396
     */
397 3
    protected function getSubCommandHelp($controller, $actionID)
398
    {
399 3
        $action = $controller->createAction($actionID);
400 3
        if ($action === null) {
401
            $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
402
            throw new Exception("No help for unknown sub-command \"$name\".");
403
        }
404
405 3
        $description = $controller->getActionHelp($action);
406 3
        if ($description !== '') {
407 2
            $this->stdout("\nDESCRIPTION\n", Console::BOLD);
408 2
            $this->stdout("\n$description\n\n");
409
        }
410
411 3
        $this->stdout("\nUSAGE\n\n", Console::BOLD);
412 3
        $scriptName = $this->getScriptName();
413 3
        if ($action->id === $controller->defaultAction) {
414 2
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
415
        } else {
416 1
            $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
417
        }
418
419 3
        $args = $controller->getActionArgsHelp($action);
420 3
        foreach ($args as $name => $arg) {
421 3
            if ($arg['required']) {
422 1
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
423
            } else {
424 3
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
425
            }
426
        }
427
428 3
        $options = $controller->getActionOptionsHelp($action);
429 3
        $options[\yii\console\Application::OPTION_APPCONFIG] = [
430 3
            'type' => 'string',
431 3
            'default' => null,
432 3
            'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
433 3
        ];
434 3
        ksort($options);
435
436 3
        if (!empty($options)) {
437 3
            $this->stdout(' [...options...]', Console::FG_RED);
438
        }
439 3
        $this->stdout("\n\n");
440
441 3
        if (!empty($args)) {
442 3
            foreach ($args as $name => $arg) {
443 3
                $this->stdout($this->formatOptionHelp(
444 3
                    '- ' . $this->ansiFormat($name, Console::FG_CYAN),
445 3
                    $arg['required'],
446 3
                    $arg['type'],
447 3
                    $arg['default'],
448 3
                    $arg['comment']
449 3
                ) . "\n\n");
450
            }
451
        }
452
453 3
        if (!empty($options)) {
454 3
            $this->stdout("\nOPTIONS\n\n", Console::BOLD);
455 3
            foreach ($options as $name => $option) {
456 3
                $this->stdout($this->formatOptionHelp(
457 3
                    $this->ansiFormat(
458 3
                        '--' . $name . $this->formatOptionAliases($controller, $name),
459 3
                        Console::FG_RED,
460 3
                        empty($option['required']) ? Console::FG_RED : Console::BOLD
461 3
                    ),
462 3
                    !empty($option['required']),
463 3
                    $option['type'],
464 3
                    $option['default'],
465 3
                    $option['comment']
466 3
                ) . "\n\n");
467
            }
468
        }
469
    }
470
471
    /**
472
     * Generates a well-formed string for an argument or option.
473
     * @param string $name the name of the argument or option
474
     * @param bool $required whether the argument is required
475
     * @param string $type the type of the option or argument
476
     * @param mixed $defaultValue the default value of the option or argument
477
     * @param string $comment comment about the option or argument
478
     * @return string the formatted string for the argument or option
479
     */
480 3
    protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
481
    {
482 3
        $comment = trim((string)$comment);
483 3
        $type = trim((string)$type);
484 3
        if (strncmp($type, 'bool', 4) === 0) {
485 3
            $type = 'boolean, 0 or 1';
486
        }
487
488 3
        if ($defaultValue !== null && !is_array($defaultValue)) {
489 3
            if ($type === null) {
0 ignored issues
show
introduced by
The condition $type === null is always false.
Loading history...
490
                $type = gettype($defaultValue);
491
            }
492 3
            if (is_bool($defaultValue)) {
493
                // show as integer to avoid confusion
494 3
                $defaultValue = (int) $defaultValue;
495
            }
496 3
            if (is_string($defaultValue)) {
497 2
                $defaultValue = "'" . $defaultValue . "'";
498
            } else {
499 3
                $defaultValue = var_export($defaultValue, true);
500
            }
501 3
            $doc = "$type (defaults to $defaultValue)";
502
        } else {
503 3
            $doc = $type;
504
        }
505
506 3
        if ($doc === '') {
507 1
            $doc = $comment;
508 3
        } elseif ($comment !== '') {
509 3
            $doc .= "\n" . preg_replace('/^/m', '  ', $comment);
510
        }
511
512 3
        $name = $required ? "$name (required)" : $name;
513
514 3
        return $doc === '' ? $name : "$name: $doc";
515
    }
516
517
    /**
518
     * @param Controller $controller the controller instance
519
     * @param string $option the option name
520
     * @return string the formatted string for the alias argument or option
521
     * @since 2.0.8
522
     */
523 3
    protected function formatOptionAliases($controller, $option)
524
    {
525 3
        foreach ($controller->optionAliases() as $name => $value) {
526 3
            if (Inflector::camel2id($value, '-', true) === $option) {
527 3
                return ', -' . $name;
528
            }
529
        }
530
531 3
        return '';
532
    }
533
534
    /**
535
     * @return string the name of the cli script currently running.
536
     */
537 6
    protected function getScriptName()
538
    {
539 6
        return basename(Yii::$app->request->scriptFile);
540
    }
541
542
    /**
543
     * Return a default help header.
544
     * @return string default help header.
545
     * @since 2.0.11
546
     */
547 2
    protected function getDefaultHelpHeader()
548
    {
549 2
        return "\nThis is Yii version " . \Yii::getVersion() . ".\n";
550
    }
551
552
    /**
553
     * Converts a CamelCase action name into an ID in lowercase.
554
     * Words in the ID are concatenated using the specified character '-'.
555
     * For example, 'CreateUser' will be converted to 'create-user'.
556
     * @param string $name the string to be converted
557
     * @return string the resulting ID
558
     */
559 24
    private function camel2id($name)
560
    {
561 24
        return mb_strtolower(trim(preg_replace('/\p{Lu}/u', '-\0', $name), '-'), 'UTF-8');
562
    }
563
}
564