You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
13 KiB
428 lines
13 KiB
<?php |
|
/** |
|
* @link http://www.yiiframework.com/ |
|
* @copyright Copyright (c) 2008 Yii Software LLC |
|
* @license http://www.yiiframework.com/license/ |
|
*/ |
|
|
|
namespace yii\console\controllers; |
|
|
|
use Yii; |
|
use yii\base\Application; |
|
use yii\base\InlineAction; |
|
use yii\console\Controller; |
|
use yii\console\Exception; |
|
use yii\helpers\Console; |
|
use yii\helpers\Inflector; |
|
|
|
/** |
|
* This command provides help information about console commands. |
|
* |
|
* This command displays the available command list in |
|
* the application or the detailed instructions about using |
|
* a specific command. |
|
* |
|
* This command can be used as follows on command line: |
|
* |
|
* ~~~ |
|
* yii help [command name] |
|
* ~~~ |
|
* |
|
* In the above, if the command name is not provided, all |
|
* available commands will be displayed. |
|
* |
|
* @property array $commands All available command names. This property is read-only. |
|
* |
|
* @author Qiang Xue <qiang.xue@gmail.com> |
|
* @since 2.0 |
|
*/ |
|
class HelpController extends Controller |
|
{ |
|
/** |
|
* Displays available commands or the detailed information |
|
* about a particular command. For example, |
|
* |
|
* ~~~ |
|
* yii help # list available commands |
|
* yii help message # display help info about "message" |
|
* ~~~ |
|
* |
|
* @param string $command The name of the command to show help about. |
|
* If not provided, all available commands will be displayed. |
|
* @return integer the exit status |
|
* @throws Exception if the command for help is unknown |
|
*/ |
|
public function actionIndex($command = null) |
|
{ |
|
if ($command !== null) { |
|
$result = Yii::$app->createController($command); |
|
if ($result === false) { |
|
throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', [ |
|
'command' => $this->ansiFormat($command, Console::FG_YELLOW), |
|
])); |
|
} |
|
|
|
list($controller, $actionID) = $result; |
|
|
|
$actions = $this->getActions($controller); |
|
if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) { |
|
$this->getActionHelp($controller, $actionID); |
|
} else { |
|
$this->getControllerHelp($controller); |
|
} |
|
} else { |
|
$this->getHelp(); |
|
} |
|
} |
|
|
|
/** |
|
* Returns all available command names. |
|
* @return array all available command names |
|
*/ |
|
public function getCommands() |
|
{ |
|
$commands = $this->getModuleCommands(Yii::$app); |
|
sort($commands); |
|
return array_unique($commands); |
|
} |
|
|
|
/** |
|
* Returns all available actions of the specified controller. |
|
* @param Controller $controller the controller instance |
|
* @return array all available action IDs. |
|
*/ |
|
public function getActions($controller) |
|
{ |
|
$actions = array_keys($controller->actions()); |
|
$class = new \ReflectionClass($controller); |
|
foreach ($class->getMethods() as $method) { |
|
$name = $method->getName(); |
|
if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') { |
|
$actions[] = Inflector::camel2id(substr($name, 6)); |
|
} |
|
} |
|
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) |
|
{ |
|
$prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/'; |
|
|
|
$commands = []; |
|
foreach (array_keys($module->controllerMap) as $id) { |
|
$commands[] = $prefix . $id; |
|
} |
|
|
|
foreach ($module->getModules() as $id => $child) { |
|
if (($child = $module->getModule($id)) === null) { |
|
continue; |
|
} |
|
foreach ($this->getModuleCommands($child) as $command) { |
|
$commands[] = $command; |
|
} |
|
} |
|
|
|
$files = scandir($module->getControllerPath()); |
|
foreach ($files as $file) { |
|
if (strcmp(substr($file, -14), 'Controller.php') === 0) { |
|
$commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14)); |
|
} |
|
} |
|
|
|
return $commands; |
|
} |
|
|
|
/** |
|
* Displays all available commands. |
|
*/ |
|
protected function getHelp() |
|
{ |
|
$commands = $this->getCommands(); |
|
if (!empty($commands)) { |
|
$this->stdout("\nThe following commands are available:\n\n", Console::BOLD); |
|
foreach ($commands as $command) { |
|
echo "- " . $this->ansiFormat($command, Console::FG_YELLOW) . "\n"; |
|
} |
|
$this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD); |
|
echo "\n yii " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' |
|
. $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n"; |
|
} else { |
|
$this->stdout("\nNo commands are found.\n\n", Console::BOLD); |
|
} |
|
} |
|
|
|
/** |
|
* Displays the overall information of the command. |
|
* @param Controller $controller the controller instance |
|
*/ |
|
protected function getControllerHelp($controller) |
|
{ |
|
$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 !== '') { |
|
$this->stdout("\nDESCRIPTION\n", Console::BOLD); |
|
echo "\n" . Console::renderColoredString($comment) . "\n\n"; |
|
} |
|
|
|
$actions = $this->getActions($controller); |
|
if (!empty($actions)) { |
|
$this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD); |
|
$prefix = $controller->getUniqueId(); |
|
foreach ($actions as $action) { |
|
echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW); |
|
if ($action === $controller->defaultAction) { |
|
$this->stdout(' (default)', Console::FG_GREEN); |
|
} |
|
$summary = $this->getActionSummary($controller, $action); |
|
if ($summary !== '') { |
|
echo ': ' . $summary; |
|
} |
|
echo "\n"; |
|
} |
|
echo "\nTo see the detailed information about individual sub-commands, enter:\n"; |
|
echo "\n yii " . $this->ansiFormat('help', Console::FG_YELLOW) . ' ' |
|
. $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n"; |
|
} |
|
} |
|
|
|
/** |
|
* 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 ''; |
|
} |
|
} |
|
|
|
/** |
|
* Displays the detailed information of a command action. |
|
* @param Controller $controller the controller instance |
|
* @param string $actionID action ID |
|
* @throws Exception if the action does not exist |
|
*/ |
|
protected function getActionHelp($controller, $actionID) |
|
{ |
|
$action = $controller->createAction($actionID); |
|
if ($action === null) { |
|
throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', [ |
|
'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'), |
|
])); |
|
} |
|
if ($action instanceof InlineAction) { |
|
$method = new \ReflectionMethod($controller, $action->actionMethod); |
|
} else { |
|
$method = new \ReflectionMethod($action, 'run'); |
|
} |
|
|
|
$tags = $this->parseComment($method->getDocComment()); |
|
$options = $this->getOptionHelps($controller); |
|
|
|
if ($tags['description'] !== '') { |
|
$this->stdout("\nDESCRIPTION\n", Console::BOLD); |
|
echo "\n" . Console::renderColoredString($tags['description']) . "\n\n"; |
|
} |
|
|
|
$this->stdout("\nUSAGE\n\n", Console::BOLD); |
|
if ($action->id === $controller->defaultAction) { |
|
echo 'yii ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW); |
|
} else { |
|
echo 'yii ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW); |
|
} |
|
list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : []); |
|
foreach ($required as $arg => $description) { |
|
$this->stdout(' <' . $arg . '>', Console::FG_CYAN); |
|
} |
|
foreach ($optional as $arg => $description) { |
|
$this->stdout(' [' . $arg . ']', Console::FG_CYAN); |
|
} |
|
if (!empty($options)) { |
|
$this->stdout(' [...options...]', Console::FG_RED); |
|
} |
|
echo "\n\n"; |
|
|
|
if (!empty($required) || !empty($optional)) { |
|
echo implode("\n\n", array_merge($required, $optional)) . "\n\n"; |
|
} |
|
|
|
$options = $this->getOptionHelps($controller); |
|
if (!empty($options)) { |
|
$this->stdout("\nOPTIONS\n\n", Console::BOLD); |
|
echo implode("\n\n", $options) . "\n\n"; |
|
} |
|
} |
|
|
|
/** |
|
* Returns the help information about arguments. |
|
* @param \ReflectionMethod $method |
|
* @param string $tags the parsed comment block related with arguments |
|
* @return array the required and optional argument help information |
|
*/ |
|
protected function getArgHelps($method, $tags) |
|
{ |
|
if (is_string($tags)) { |
|
$tags = [$tags]; |
|
} |
|
$params = $method->getParameters(); |
|
$optional = $required = []; |
|
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()) { |
|
$optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment); |
|
} else { |
|
$required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment); |
|
} |
|
} |
|
|
|
return [$required, $optional]; |
|
} |
|
|
|
/** |
|
* Returns the help information about the options available for a console controller. |
|
* @param Controller $controller the console controller |
|
* @return array the help information about the options |
|
*/ |
|
protected function getOptionHelps($controller) |
|
{ |
|
$optionNames = $controller->globalOptions(); |
|
if (empty($optionNames)) { |
|
return []; |
|
} |
|
|
|
$class = new \ReflectionClass($controller); |
|
$options = []; |
|
foreach ($class->getProperties() as $property) { |
|
$name = $property->getName(); |
|
if (!in_array($name, $optionNames, true)) { |
|
continue; |
|
} |
|
$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); |
|
} |
|
if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) { |
|
$type = $matches[1]; |
|
$comment = $matches[2]; |
|
} else { |
|
$type = null; |
|
$comment = $doc; |
|
} |
|
$options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment); |
|
} else { |
|
$options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, ''); |
|
} |
|
} |
|
ksort($options); |
|
return $options; |
|
} |
|
|
|
/** |
|
* Parses the comment block into tags. |
|
* @param string $comment the comment block |
|
* @return array the parsed tags |
|
*/ |
|
protected function parseComment($comment) |
|
{ |
|
$tags = []; |
|
$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 { |
|
$tags[$name] = [$tags[$name], trim($matches[2])]; |
|
} |
|
} |
|
} |
|
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); |
|
} |
|
if (is_bool($defaultValue)) { |
|
// show as integer to avoid confusion |
|
$defaultValue = (int)$defaultValue; |
|
} |
|
$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"; |
|
} |
|
}
|
|
|