HelpController.php 14.3 KB
Newer Older
Alexander Makarov committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Alexander Makarov committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

8
namespace yii\console\controllers;
Alexander Makarov committed
9

10
use Yii;
Qiang Xue committed
11 12 13
use yii\base\Application;
use yii\base\InlineAction;
use yii\console\Controller;
Qiang Xue committed
14
use yii\console\Exception;
15
use yii\helpers\Console;
Alexander Makarov committed
16
use yii\helpers\Inflector;
Qiang Xue committed
17

Alexander Makarov committed
18
/**
19
 * Provides help information about console commands.
Alexander Makarov committed
20
 *
Qiang Xue committed
21 22 23
 * This command displays the available command list in
 * the application or the detailed instructions about using
 * a specific command.
Alexander Makarov committed
24
 *
Qiang Xue committed
25
 * This command can be used as follows on command line:
Qiang Xue committed
26 27
 *
 * ~~~
28
 * yii help [command name]
Qiang Xue committed
29 30
 * ~~~
 *
Qiang Xue committed
31 32
 * In the above, if the command name is not provided, all
 * available commands will be displayed.
Alexander Makarov committed
33
 *
34
 * @property array $commands All available command names. This property is read-only.
35
 *
Alexander Makarov committed
36 37 38
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
Qiang Xue committed
39
class HelpController extends Controller
Alexander Makarov committed
40
{
Qiang Xue committed
41 42 43 44
	/**
	 * Displays available commands or the detailed information
	 * about a particular command. For example,
	 *
Qiang Xue committed
45
	 * @param string $command The name of the command to show help about.
Qiang Xue committed
46
	 * If not provided, all available commands will be displayed.
Qiang Xue committed
47
	 * @return integer the exit status
Qiang Xue committed
48
	 * @throws Exception if the command for help is unknown
Qiang Xue committed
49
	 */
Qiang Xue committed
50
	public function actionIndex($command = null)
Qiang Xue committed
51
	{
Qiang Xue committed
52
		if ($command !== null) {
Qiang Xue committed
53
			$result = Yii::$app->createController($command);
Qiang Xue committed
54
			if ($result === false) {
Alexander Makarov committed
55
				throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', [
56
					'command' => $this->ansiFormat($command, Console::FG_YELLOW),
Alexander Makarov committed
57
				]));
Qiang Xue committed
58 59
			}

Qiang Xue committed
60
			list($controller, $actionID) = $result;
Qiang Xue committed
61

Qiang Xue committed
62 63
			$actions = $this->getActions($controller);
			if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
64
				$this->getActionHelp($controller, $actionID);
Qiang Xue committed
65 66
			} else {
				$this->getControllerHelp($controller);
Qiang Xue committed
67
			}
Qiang Xue committed
68 69
		} else {
			$this->getHelp();
Qiang Xue committed
70 71 72 73 74 75 76 77 78
		}
	}

	/**
	 * Returns all available command names.
	 * @return array all available command names
	 */
	public function getCommands()
	{
Qiang Xue committed
79
		$commands = $this->getModuleCommands(Yii::$app);
Qiang Xue committed
80 81 82 83
		sort($commands);
		return array_unique($commands);
	}

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
	/**
	 * Returns an array of commands an their descriptions.
	 * @return array all available commands as keys and their description as values.
	 */
	protected function getCommandDescriptions()
	{
		$descriptions = [];
		foreach ($this->getCommands() as $command) {
			$description = '';

			$result = Yii::$app->createController($command);
			if ($result !== false) {
				list($controller, $actionID) = $result;
				$class = new \ReflectionClass($controller);

				$docLines = preg_split('~(\n|\r|\r\n)~', $class->getDocComment());
				if (isset($docLines[1])) {
					$description = trim($docLines[1], ' *');
				}
			}

			$descriptions[$command] = $description;
		}
		return $descriptions;
	}

Qiang Xue committed
110 111 112 113 114 115 116
	/**
	 * Returns all available actions of the specified controller.
	 * @param Controller $controller the controller instance
	 * @return array all available action IDs.
	 */
	public function getActions($controller)
	{
Qiang Xue committed
117
		$actions = array_keys($controller->actions());
Qiang Xue committed
118 119 120
		$class = new \ReflectionClass($controller);
		foreach ($class->getMethods() as $method) {
			$name = $method->getName();
Qiang Xue committed
121
			if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') {
Alexander Makarov committed
122
				$actions[] = Inflector::camel2id(substr($name, 6));
Qiang Xue committed
123 124 125 126 127 128 129 130 131 132 133 134 135
			}
		}
		sort($actions);
		return array_unique($actions);
	}

	/**
	 * Returns available commands of a specified module.
	 * @param \yii\base\Module $module the module instance
	 * @return array the available command names
	 */
	protected function getModuleCommands($module)
	{
Qiang Xue committed
136
		$prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/';
Qiang Xue committed
137

Alexander Makarov committed
138
		$commands = [];
Qiang Xue committed
139
		foreach (array_keys($module->controllerMap) as $id) {
Qiang Xue committed
140 141 142 143 144 145 146 147
			$commands[] = $prefix . $id;
		}

		foreach ($module->getModules() as $id => $child) {
			if (($child = $module->getModule($id)) === null) {
				continue;
			}
			foreach ($this->getModuleCommands($child) as $command) {
148
				$commands[] = $command;
Qiang Xue committed
149 150 151
			}
		}

152 153 154 155 156 157 158
		$controllerPath = $module->getControllerPath();
		if (is_dir($controllerPath)) {
			$files = scandir($controllerPath);
			foreach ($files as $file) {
				if (strcmp(substr($file, -14), 'Controller.php') === 0) {
					$commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14));
				}
Qiang Xue committed
159 160 161 162 163 164 165 166 167
			}
		}

		return $commands;
	}

	/**
	 * Displays all available commands.
	 */
Qiang Xue committed
168
	protected function getHelp()
Qiang Xue committed
169
	{
170
		$commands = $this->getCommandDescriptions();
171
		if (!empty($commands)) {
172
			$this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
173 174 175 176 177 178 179 180 181 182
			$len = 0;
			foreach ($commands as $command => $description) {
				if (($l = strlen($command)) > $len) {
					$len = $l;
				}
			}
			foreach ($commands as $command => $description) {
				echo "- " . $this->ansiFormat($command, Console::FG_YELLOW);
				echo str_repeat(' ', $len + 3 - strlen($command)) . $description;
				echo "\n";
Qiang Xue committed
183
			}
184
			$scriptName = $this->getScriptName();
185
			$this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
186
			echo "\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
187
							. $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n";
Qiang Xue committed
188
		} else {
189
			$this->stdout("\nNo commands are found.\n\n", Console::BOLD);
Qiang Xue committed
190 191
		}
	}
Qiang Xue committed
192

Qiang Xue committed
193 194 195 196
	/**
	 * Displays the overall information of the command.
	 * @param Controller $controller the controller instance
	 */
Qiang Xue committed
197
	protected function getControllerHelp($controller)
Qiang Xue committed
198
	{
Qiang Xue committed
199 200 201 202 203 204 205
		$class = new \ReflectionClass($controller);
		$comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($class->getDocComment(), '/'))), "\r", '');
		if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
			$comment = trim(substr($comment, 0, $matches[0][1]));
		}

		if ($comment !== '') {
206 207
			$this->stdout("\nDESCRIPTION\n", Console::BOLD);
			echo "\n" . Console::renderColoredString($comment) . "\n\n";
Qiang Xue committed
208 209 210
		}

		$actions = $this->getActions($controller);
211
		if (!empty($actions)) {
212
			$this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
Qiang Xue committed
213 214
			$prefix = $controller->getUniqueId();
			foreach ($actions as $action) {
215
				echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW);
Qiang Xue committed
216
				if ($action === $controller->defaultAction) {
217
					$this->stdout(' (default)', Console::FG_GREEN);
Qiang Xue committed
218
				}
Qiang Xue committed
219 220 221 222 223
				$summary = $this->getActionSummary($controller, $action);
				if ($summary !== '') {
					echo ': ' . $summary;
				}
				echo "\n";
Qiang Xue committed
224
			}
225
			$scriptName = $this->getScriptName();
226
			echo "\nTo see the detailed information about individual sub-commands, enter:\n";
227
			echo "\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
228
							. $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n";
Qiang Xue committed
229
		}
Qiang Xue committed
230 231
	}

Qiang Xue committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
	/**
	 * Returns the short summary of the action.
	 * @param Controller $controller the controller instance
	 * @param string $actionID action ID
	 * @return string the summary about the action
	 */
	protected function getActionSummary($controller, $actionID)
	{
		$action = $controller->createAction($actionID);
		if ($action === null) {
			return '';
		}
		if ($action instanceof InlineAction) {
			$reflection = new \ReflectionMethod($controller, $action->actionMethod);
		} else {
			$reflection = new \ReflectionClass($action);
		}
		$tags = $this->parseComment($reflection->getDocComment());
		if ($tags['description'] !== '') {
			$limit = 73 - strlen($action->getUniqueId());
			if ($actionID === $controller->defaultAction) {
				$limit -= 10;
			}
			if ($limit < 0) {
				$limit = 50;
			}
			$description = $tags['description'];
			if (($pos = strpos($tags['description'], "\n")) !== false) {
				$description = substr($description, 0, $pos);
			}
			$text = substr($description, 0, $limit);
			return strlen($description) > $limit ? $text . '...' : $text;
		} else {
			return '';
		}
	}

Alexander Makarov committed
269
	/**
Qiang Xue committed
270 271 272
	 * Displays the detailed information of a command action.
	 * @param Controller $controller the controller instance
	 * @param string $actionID action ID
Qiang Xue committed
273
	 * @throws Exception if the action does not exist
Alexander Makarov committed
274
	 */
Qiang Xue committed
275
	protected function getActionHelp($controller, $actionID)
Alexander Makarov committed
276
	{
Qiang Xue committed
277 278
		$action = $controller->createAction($actionID);
		if ($action === null) {
Alexander Makarov committed
279
			throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', [
280
				'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'),
Alexander Makarov committed
281
			]));
Qiang Xue committed
282 283
		}
		if ($action instanceof InlineAction) {
Qiang Xue committed
284
			$method = new \ReflectionMethod($controller, $action->actionMethod);
Qiang Xue committed
285 286 287
		} else {
			$method = new \ReflectionMethod($action, 'run');
		}
Qiang Xue committed
288 289

		$tags = $this->parseComment($method->getDocComment());
290
		$options = $this->getOptionHelps($controller, $actionID);
Qiang Xue committed
291 292

		if ($tags['description'] !== '') {
293 294
			$this->stdout("\nDESCRIPTION\n", Console::BOLD);
			echo "\n" . Console::renderColoredString($tags['description']) . "\n\n";
Qiang Xue committed
295
		}
Qiang Xue committed
296

297
		$this->stdout("\nUSAGE\n\n", Console::BOLD);
298
		$scriptName = $this->getScriptName();
Qiang Xue committed
299
		if ($action->id === $controller->defaultAction) {
300
			echo $scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW);
Qiang Xue committed
301
		} else {
302
			echo $scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW);
Qiang Xue committed
303
		}
Alexander Makarov committed
304
		list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : []);
305 306
		foreach ($required as $arg => $description) {
			$this->stdout(' <' . $arg . '>', Console::FG_CYAN);
Alexander Makarov committed
307
		}
308 309
		foreach ($optional as $arg => $description) {
			$this->stdout(' [' . $arg . ']', Console::FG_CYAN);
Qiang Xue committed
310
		}
311
		if (!empty($options)) {
312
			$this->stdout(' [...options...]', Console::FG_RED);
313
		}
Qiang Xue committed
314
		echo "\n\n";
Qiang Xue committed
315

Qiang Xue committed
316 317
		if (!empty($required) || !empty($optional)) {
			echo implode("\n\n", array_merge($required, $optional)) . "\n\n";
Qiang Xue committed
318
		}
Qiang Xue committed
319

320
		if (!empty($options)) {
321
			$this->stdout("\nOPTIONS\n\n", Console::BOLD);
Qiang Xue committed
322 323 324 325
			echo implode("\n\n", $options) . "\n\n";
		}
	}

Qiang Xue committed
326
	/**
Qiang Xue committed
327
	 * Returns the help information about arguments.
Qiang Xue committed
328
	 * @param \ReflectionMethod $method
Qiang Xue committed
329 330
	 * @param string $tags the parsed comment block related with arguments
	 * @return array the required and optional argument help information
Qiang Xue committed
331
	 */
Qiang Xue committed
332
	protected function getArgHelps($method, $tags)
Qiang Xue committed
333
	{
Qiang Xue committed
334
		if (is_string($tags)) {
Alexander Makarov committed
335
			$tags = [$tags];
Qiang Xue committed
336
		}
Qiang Xue committed
337
		$params = $method->getParameters();
Alexander Makarov committed
338
		$optional = $required = [];
Qiang Xue committed
339 340 341 342 343 344 345 346 347 348 349
		foreach ($params as $i => $param) {
			$name = $param->getName();
			$tag = isset($tags[$i]) ? $tags[$i] : '';
			if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
				$type = $matches[1];
				$comment = $matches[3];
			} else {
				$type = null;
				$comment = $tag;
			}
			if ($param->isDefaultValueAvailable()) {
350
				$optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment);
Qiang Xue committed
351
			} else {
352
				$required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment);
Qiang Xue committed
353 354
			}
		}
Qiang Xue committed
355

Alexander Makarov committed
356
		return [$required, $optional];
Qiang Xue committed
357 358 359
	}

	/**
Qiang Xue committed
360 361
	 * Returns the help information about the options available for a console controller.
	 * @param Controller $controller the console controller
362
	 * @param string $actionID name of the action, if set include local options for that action
Qiang Xue committed
363
	 * @return array the help information about the options
Qiang Xue committed
364
	 */
365
	protected function getOptionHelps($controller, $actionID)
Qiang Xue committed
366
	{
367
		$optionNames = $controller->options($actionID);
Qiang Xue committed
368
		if (empty($optionNames)) {
Alexander Makarov committed
369
			return [];
Qiang Xue committed
370 371 372
		}

		$class = new \ReflectionClass($controller);
Alexander Makarov committed
373
		$options = [];
Qiang Xue committed
374 375
		foreach ($class->getProperties() as $property) {
			$name = $property->getName();
Qiang Xue committed
376 377
			if (!in_array($name, $optionNames, true)) {
				continue;
Qiang Xue committed
378
			}
Qiang Xue committed
379 380 381 382 383 384
			$defaultValue = $property->getValue($controller);
			$tags = $this->parseComment($property->getDocComment());
			if (isset($tags['var']) || isset($tags['property'])) {
				$doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
				if (is_array($doc)) {
					$doc = reset($doc);
Qiang Xue committed
385
				}
Qiang Xue committed
386 387 388 389 390 391 392
				if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) {
					$type = $matches[1];
					$comment = $matches[2];
				} else {
					$type = null;
					$comment = $doc;
				}
393
				$options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment);
Qiang Xue committed
394
			} else {
395
				$options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, '');
Qiang Xue committed
396 397 398 399
			}
		}
		ksort($options);
		return $options;
Alexander Makarov committed
400
	}
Qiang Xue committed
401 402 403 404 405 406 407 408

	/**
	 * Parses the comment block into tags.
	 * @param string $comment the comment block
	 * @return array the parsed tags
	 */
	protected function parseComment($comment)
	{
Alexander Makarov committed
409
		$tags = [];
Qiang Xue committed
410 411 412 413 414 415 416 417 418 419
		$comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
		$parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
		foreach ($parts as $part) {
			if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
				$name = $matches[1];
				if (!isset($tags[$name])) {
					$tags[$name] = trim($matches[2]);
				} elseif (is_array($tags[$name])) {
					$tags[$name][] = trim($matches[2]);
				} else {
Alexander Makarov committed
420
					$tags[$name] = [$tags[$name], trim($matches[2])];
Qiang Xue committed
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
				}
			}
		}
		return $tags;
	}

	/**
	 * Generates a well-formed string for an argument or option.
	 * @param string $name the name of the argument or option
	 * @param boolean $required whether the argument is required
	 * @param string $type the type of the option or argument
	 * @param mixed $defaultValue the default value of the option or argument
	 * @param string $comment comment about the option or argument
	 * @return string the formatted string for the argument or option
	 */
	protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
	{
		$doc = '';
		$comment = trim($comment);

		if ($defaultValue !== null && !is_array($defaultValue)) {
			if ($type === null) {
				$type = gettype($defaultValue);
			}
445 446 447 448
			if (is_bool($defaultValue)) {
				// show as integer to avoid confusion
				$defaultValue = (int)$defaultValue;
			}
Qiang Xue committed
449 450 451 452 453 454 455 456 457 458 459 460 461 462
			$doc = "$type (defaults to " . var_export($defaultValue, true) . ")";
		} elseif (trim($type) !== '') {
			$doc = $type;
		}

		if ($doc === '') {
			$doc = $comment;
		} elseif ($comment !== '') {
			$doc .= "\n" . preg_replace("/^/m", "  ", $comment);
		}

		$name = $required ? "$name (required)" : $name;
		return $doc === '' ? $name : "$name: $doc";
	}
463 464 465 466 467 468 469 470

	/**
	 * @return string the name of the cli script currently running.
	 */
	protected function getScriptName()
	{
		return basename(Yii::$app->request->scriptFile);
	}
Zander Baldwin committed
471
}