Completed
Push — master ( 56f639...2b9374 )
by Alexander
20:28
created

HelpController::camel2id()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\base\Application;
12
use yii\console\Controller;
13
use yii\console\Exception;
14
use yii\helpers\Console;
15
use yii\helpers\Inflector;
16
17
/**
18
 * Provides help information about console commands.
19
 *
20
 * This command displays the available command list in
21
 * the application or the detailed instructions about using
22
 * a specific command.
23
 *
24
 * This command can be used as follows on command line:
25
 *
26
 * ```
27
 * yii help [command name]
28
 * ```
29
 *
30
 * In the above, if the command name is not provided, all
31
 * available commands will be displayed.
32
 *
33
 * @property array $commands All available command names. This property is read-only.
34
 *
35
 * @author Qiang Xue <[email protected]>
36
 * @since 2.0
37
 */
38
class HelpController extends Controller
39
{
40
    /**
41
     * Displays available commands or the detailed information
42
     * about a particular command.
43
     *
44
     * @param string $command The name of the command to show help about.
45
     * If not provided, all available commands will be displayed.
46
     * @return int the exit status
47
     * @throws Exception if the command for help is unknown
48
     */
49 4
    public function actionIndex($command = null)
50
    {
51 4
        if ($command !== null) {
52 3
            $result = Yii::$app->createController($command);
53 3
            if ($result === false) {
0 ignored issues
show
introduced by
The condition $result === false is always false.
Loading history...
54
                $name = $this->ansiFormat($command, Console::FG_YELLOW);
55
                throw new Exception("No help for unknown command \"$name\".");
56
            }
57
58 3
            list($controller, $actionID) = $result;
59
60 3
            $actions = $this->getActions($controller);
61 3
            if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
62 3
                $this->getSubCommandHelp($controller, $actionID);
63
            } else {
64 3
                $this->getCommandHelp($controller);
65
            }
66
        } else {
67 1
            $this->getDefaultHelp();
68
        }
69 4
    }
70
71
    /**
72
     * List all available controllers and actions in machine readable format.
73
     * This is used for shell completion.
74
     * @since 2.0.11
75
     */
76 2
    public function actionList()
77
    {
78 2
        foreach ($this->getCommandDescriptions() as $command => $description) {
79 2
            $result = Yii::$app->createController($command);
80 2
            if ($result === false || !($result[0] instanceof Controller)) {
81
                continue;
82
            }
83
            /** @var $controller Controller */
84 2
            list($controller, $actionID) = $result;
85 2
            $actions = $this->getActions($controller);
86 2
            if (!empty($actions)) {
87 2
                $prefix = $controller->getUniqueId();
88 2
                $this->stdout("$prefix\n");
89 2
                foreach ($actions as $action) {
90 2
                    $this->stdout("$prefix/$action\n");
91
                }
92
            }
93
        }
94 2
    }
95
96
    /**
97
     * List all available options for the $action in machine readable format.
98
     * This is used for shell completion.
99
     *
100
     * @param string $action route to action
101
     * @since 2.0.11
102
     */
103 1
    public function actionListActionOptions($action)
104
    {
105 1
        $result = Yii::$app->createController($action);
106
107 1
        if ($result === false || !($result[0] instanceof Controller)) {
108
            return;
109
        }
110
111
        /** @var Controller $controller */
112 1
        list($controller, $actionID) = $result;
113 1
        $action = $controller->createAction($actionID);
114 1
        if ($action === null) {
115
            return;
116
        }
117
118 1
        foreach ($controller->getActionArgsHelp($action) as $argument => $help) {
119 1
            $description = preg_replace("~\R~", '', addcslashes($help['comment'], ':')) ?: $argument;
120 1
            $this->stdout($argument . ':' . $description . "\n");
121
        }
122
123 1
        $this->stdout("\n");
124 1
        foreach ($controller->getActionOptionsHelp($action) as $argument => $help) {
125 1
            $description = preg_replace("~\R~", '', addcslashes($help['comment'], ':'));
126 1
            $this->stdout('--' . $argument . ($description ? ':' . $description : '') . "\n");
127
        }
128 1
    }
129
130
    /**
131
     * Displays usage information for $action.
132
     *
133
     * @param string $action route to action
134
     * @since 2.0.11
135
     */
136 1
    public function actionUsage($action)
137
    {
138 1
        $result = Yii::$app->createController($action);
139
140 1
        if ($result === false || !($result[0] instanceof Controller)) {
141
            return;
142
        }
143
144
        /** @var Controller $controller */
145 1
        list($controller, $actionID) = $result;
146 1
        $action = $controller->createAction($actionID);
147 1
        if ($action === null) {
148
            return;
149
        }
150
151 1
        $scriptName = $this->getScriptName();
152 1
        if ($action->id === $controller->defaultAction) {
153
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
154
        } else {
155 1
            $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
156
        }
157
158 1
        foreach ($controller->getActionArgsHelp($action) as $name => $arg) {
159 1
            if ($arg['required']) {
160 1
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
161
            } else {
162 1
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
163
            }
164
        }
165
166 1
        $this->stdout("\n");
167 1
    }
168
169
    /**
170
     * Returns all available command names.
171
     * @return array all available command names
172
     */
173 18
    public function getCommands()
174
    {
175 18
        $commands = $this->getModuleCommands(Yii::$app);
176 18
        sort($commands);
177 18
        return array_unique($commands);
178
    }
179
180
    /**
181
     * Returns an array of commands an their descriptions.
182
     * @return array all available commands as keys and their description as values.
183
     */
184 3
    protected function getCommandDescriptions()
185
    {
186 3
        $descriptions = [];
187 3
        foreach ($this->getCommands() as $command) {
188 3
            $description = '';
189
190 3
            $result = Yii::$app->createController($command);
191 3
            if ($result !== false && $result[0] instanceof Controller) {
192 3
                list($controller, $actionID) = $result;
193
                /** @var Controller $controller */
194 3
                $description = $controller->getHelpSummary();
195
            }
196
197 3
            $descriptions[$command] = $description;
198
        }
199
200 3
        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 21
    public function getActions($controller)
209
    {
210 21
        $actions = array_keys($controller->actions());
211 21
        $class = new \ReflectionClass($controller);
212 21
        foreach ($class->getMethods() as $method) {
213 21
            $name = $method->getName();
214 21
            if ($name !== 'actions' && $method->isPublic() && !$method->isStatic() && strncmp($name, 'action', 6) === 0) {
215 21
                $actions[] = $this->camel2id(substr($name, 6));
216
            }
217
        }
218 21
        sort($actions);
219
220 21
        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 18
    protected function getModuleCommands($module)
229
    {
230 18
        $prefix = $module instanceof Application ? '' : $module->getUniqueId() . '/';
231
232 18
        $commands = [];
233 18
        foreach (array_keys($module->controllerMap) as $id) {
234 18
            $commands[] = $prefix . $id;
235
        }
236
237 18
        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 18
        $controllerPath = $module->getControllerPath();
247 18
        if (is_dir($controllerPath)) {
248 1
            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($controllerPath, \RecursiveDirectoryIterator::KEY_AS_PATHNAME));
249 1
            $iterator = new \RegexIterator($iterator, '/.*Controller\.php$/', \RecursiveRegexIterator::GET_MATCH);
250 1
            foreach ($iterator as $matches) {
251 1
                $file = $matches[0];
252 1
                $relativePath = str_replace($controllerPath, '', $file);
253 1
                $class = strtr($relativePath, [
254 1
                    '/' => '\\',
255
                    '.php' => '',
256
                ]);
257 1
                $controllerClass = $module->controllerNamespace . $class;
258 1
                if ($this->validateControllerClass($controllerClass)) {
259 1
                    $dir = ltrim(pathinfo($relativePath, PATHINFO_DIRNAME), '\\/');
260
261 1
                    $command = Inflector::camel2id(substr(basename($file), 0, -14), '-', true);
262 1
                    if (!empty($dir)) {
263 1
                        $command = $dir . '/' . $command;
264
                    }
265 1
                    $commands[] = $prefix . $command;
266
                }
267
            }
268
        }
269
270 18
        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 1
    protected function validateControllerClass($controllerClass)
279
    {
280 1
        if (class_exists($controllerClass)) {
281 1
            $class = new \ReflectionClass($controllerClass);
282 1
            return !$class->isAbstract() && $class->isSubclassOf('yii\console\Controller');
283
        }
284
285
        return false;
286
    }
287
288
    /**
289
     * Displays all available commands.
290
     */
291 1
    protected function getDefaultHelp()
292
    {
293 1
        $commands = $this->getCommandDescriptions();
294 1
        $this->stdout($this->getDefaultHelpHeader());
295 1
        if (!empty($commands)) {
296 1
            $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
297 1
            $len = 0;
298 1
            foreach ($commands as $command => $description) {
299 1
                $result = Yii::$app->createController($command);
300 1
                if ($result !== false && $result[0] instanceof Controller) {
301
                    /** @var $controller Controller */
302 1
                    list($controller, $actionID) = $result;
303 1
                    $actions = $this->getActions($controller);
304 1
                    if (!empty($actions)) {
305 1
                        $prefix = $controller->getUniqueId();
306 1
                        foreach ($actions as $action) {
307 1
                            $string = $prefix . '/' . $action;
308 1
                            if ($action === $controller->defaultAction) {
309 1
                                $string .= ' (default)';
310
                            }
311 1
                            if (($l = strlen($string)) > $len) {
312 1
                                $len = $l;
313
                            }
314
                        }
315
                    }
316
                } elseif (($l = strlen($command)) > $len) {
317 1
                    $len = $l;
318
                }
319
            }
320 1
            foreach ($commands as $command => $description) {
321 1
                $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
322 1
                $this->stdout(str_repeat(' ', $len + 4 - strlen($command)));
323 1
                $this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD);
324 1
                $this->stdout("\n");
325
326 1
                $result = Yii::$app->createController($command);
327 1
                if ($result !== false && $result[0] instanceof Controller) {
328 1
                    list($controller, $actionID) = $result;
329 1
                    $actions = $this->getActions($controller);
330 1
                    if (!empty($actions)) {
331 1
                        $prefix = $controller->getUniqueId();
332 1
                        foreach ($actions as $action) {
333 1
                            $string = '  ' . $prefix . '/' . $action;
334 1
                            $this->stdout('  ' . $this->ansiFormat($string, Console::FG_GREEN));
335 1
                            if ($action === $controller->defaultAction) {
336 1
                                $string .= ' (default)';
337 1
                                $this->stdout(' (default)', Console::FG_YELLOW);
338
                            }
339 1
                            $summary = $controller->getActionHelpSummary($controller->createAction($action));
340 1
                            if ($summary !== '') {
341 1
                                $this->stdout(str_repeat(' ', $len + 4 - strlen($string)));
342 1
                                $this->stdout(Console::wrapText($summary, $len + 4 + 2));
343
                            }
344 1
                            $this->stdout("\n");
345
                        }
346
                    }
347 1
                    $this->stdout("\n");
348
                }
349
            }
350 1
            $scriptName = $this->getScriptName();
351 1
            $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
352 1
            $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
353 1
                . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
354
        } else {
355
            $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
356
        }
357 1
    }
358
359
    /**
360
     * Displays the overall information of the command.
361
     * @param Controller $controller the controller instance
362
     */
363
    protected function getCommandHelp($controller)
364
    {
365
        $controller->color = $this->color;
366
367
        $this->stdout("\nDESCRIPTION\n", Console::BOLD);
368
        $comment = $controller->getHelp();
369
        if ($comment !== '') {
370
            $this->stdout("\n$comment\n\n");
371
        }
372
373
        $actions = $this->getActions($controller);
374
        if (!empty($actions)) {
375
            $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
376
            $prefix = $controller->getUniqueId();
377
378
            $maxlen = 5;
379
            foreach ($actions as $action) {
380
                $len = strlen($prefix . '/' . $action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
381
                if ($maxlen < $len) {
382
                    $maxlen = $len;
383
                }
384
            }
385
            foreach ($actions as $action) {
386
                $this->stdout('- ' . $this->ansiFormat($prefix . '/' . $action, Console::FG_YELLOW));
387
                $len = strlen($prefix . '/' . $action) + 2;
388
                if ($action === $controller->defaultAction) {
389
                    $this->stdout(' (default)', Console::FG_GREEN);
390
                    $len += 10;
391
                }
392
                $summary = $controller->getActionHelpSummary($controller->createAction($action));
393
                if ($summary !== '') {
394
                    $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
395
                }
396
                $this->stdout("\n");
397
            }
398
            $scriptName = $this->getScriptName();
399
            $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
400
            $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
401
                . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
402
        }
403
    }
404
405
    /**
406
     * Displays the detailed information of a command action.
407
     * @param Controller $controller the controller instance
408
     * @param string $actionID action ID
409
     * @throws Exception if the action does not exist
410
     */
411 3
    protected function getSubCommandHelp($controller, $actionID)
412
    {
413 3
        $action = $controller->createAction($actionID);
414 3
        if ($action === null) {
415
            $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
416
            throw new Exception("No help for unknown sub-command \"$name\".");
417
        }
418
419 3
        $description = $controller->getActionHelp($action);
420 3
        if ($description !== '') {
421 2
            $this->stdout("\nDESCRIPTION\n", Console::BOLD);
422 2
            $this->stdout("\n$description\n\n");
423
        }
424
425 3
        $this->stdout("\nUSAGE\n\n", Console::BOLD);
426 3
        $scriptName = $this->getScriptName();
427 3
        if ($action->id === $controller->defaultAction) {
428 2
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
429
        } else {
430 1
            $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
431
        }
432
433 3
        $args = $controller->getActionArgsHelp($action);
434 3
        foreach ($args as $name => $arg) {
435 3
            if ($arg['required']) {
436 1
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
437
            } else {
438 3
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
439
            }
440
        }
441
442 3
        $options = $controller->getActionOptionsHelp($action);
443 3
        $options[\yii\console\Application::OPTION_APPCONFIG] = [
444
            'type' => 'string',
445
            'default' => null,
446
            'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
447
        ];
448 3
        ksort($options);
449
450 3
        if (!empty($options)) {
451 3
            $this->stdout(' [...options...]', Console::FG_RED);
452
        }
453 3
        $this->stdout("\n\n");
454
455 3
        if (!empty($args)) {
456 3
            foreach ($args as $name => $arg) {
457 3
                $this->stdout($this->formatOptionHelp(
458 3
                        '- ' . $this->ansiFormat($name, Console::FG_CYAN),
459 3
                        $arg['required'],
460 3
                        $arg['type'],
461 3
                        $arg['default'],
462 3
                        $arg['comment']) . "\n\n");
463
            }
464
        }
465
466 3
        if (!empty($options)) {
467 3
            $this->stdout("\nOPTIONS\n\n", Console::BOLD);
468 3
            foreach ($options as $name => $option) {
469 3
                $this->stdout($this->formatOptionHelp(
470 3
                        $this->ansiFormat('--' . $name . $this->formatOptionAliases($controller, $name),
471 3
                            Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
472 3
                        !empty($option['required']),
473 3
                        $option['type'],
474 3
                        $option['default'],
475 3
                        $option['comment']) . "\n\n");
476
            }
477
        }
478 3
    }
479
480
    /**
481
     * Generates a well-formed string for an argument or option.
482
     * @param string $name the name of the argument or option
483
     * @param bool $required whether the argument is required
484
     * @param string $type the type of the option or argument
485
     * @param mixed $defaultValue the default value of the option or argument
486
     * @param string $comment comment about the option or argument
487
     * @return string the formatted string for the argument or option
488
     */
489 3
    protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
490
    {
491 3
        $comment = trim($comment);
492 3
        $type = trim($type);
493 3
        if (strncmp($type, 'bool', 4) === 0) {
494 3
            $type = 'boolean, 0 or 1';
495
        }
496
497 3
        if ($defaultValue !== null && !is_array($defaultValue)) {
498 3
            if ($type === null) {
0 ignored issues
show
introduced by
The condition $type === null is always false.
Loading history...
499
                $type = gettype($defaultValue);
500
            }
501 3
            if (is_bool($defaultValue)) {
502
                // show as integer to avoid confusion
503 3
                $defaultValue = (int)$defaultValue;
504
            }
505 3
            if (is_string($defaultValue)) {
506 2
                $defaultValue = "'" . $defaultValue . "'";
507
            } else {
508 3
                $defaultValue = var_export($defaultValue, true);
509
            }
510 3
            $doc = "$type (defaults to $defaultValue)";
511
        } else {
512 3
            $doc = $type;
513
        }
514
515 3
        if ($doc === '') {
516 1
            $doc = $comment;
517 3
        } elseif ($comment !== '') {
518 3
            $doc .= "\n" . preg_replace('/^/m', '  ', $comment);
519
        }
520
521 3
        $name = $required ? "$name (required)" : $name;
522
523 3
        return $doc === '' ? $name : "$name: $doc";
524
    }
525
526
    /**
527
     * @param Controller $controller the controller instance
528
     * @param string $option the option name
529
     * @return string the formatted string for the alias argument or option
530
     * @since 2.0.8
531
     */
532 3
    protected function formatOptionAliases($controller, $option)
533
    {
534 3
        foreach ($controller->optionAliases() as $name => $value) {
535 3
            if (Inflector::camel2id($value, '-', true) === $option) {
536 3
                return ', -' . $name;
537
            }
538
        }
539
540 3
        return '';
541
    }
542
543
    /**
544
     * @return string the name of the cli script currently running.
545
     */
546 5
    protected function getScriptName()
547
    {
548 5
        return basename(Yii::$app->request->scriptFile);
549
    }
550
551
    /**
552
     * Return a default help header.
553
     * @return string default help header.
554
     * @since 2.0.11
555
     */
556 1
    protected function getDefaultHelpHeader()
557
    {
558 1
        return "\nThis is Yii version " . \Yii::getVersion() . ".\n";
559
    }
560
561
    /**
562
     * Converts a CamelCase action name into an ID in lowercase.
563
     * Words in the ID are concatenated using the specified character '-'.
564
     * For example, 'CreateUser' will be converted to 'create-user'.
565
     * @param string $name the string to be converted
566
     * @return string the resulting ID
567
     */
568 21
    private function camel2id($name)
569
    {
570 21
        return mb_strtolower(trim(preg_replace('/\p{Lu}/u', '-\0', $name), '-'), 'UTF-8');
571
    }
572
}
573