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.

372 lines
13KB

  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. namespace yii\console\controllers;
  8. use Yii;
  9. use yii\base\Application;
  10. use yii\console\Controller;
  11. use yii\console\Exception;
  12. use yii\helpers\Console;
  13. use yii\helpers\Inflector;
  14. /**
  15. * Provides help information about console commands.
  16. *
  17. * This command displays the available command list in
  18. * the application or the detailed instructions about using
  19. * a specific command.
  20. *
  21. * This command can be used as follows on command line:
  22. *
  23. * ~~~
  24. * yii help [command name]
  25. * ~~~
  26. *
  27. * In the above, if the command name is not provided, all
  28. * available commands will be displayed.
  29. *
  30. * @property array $commands All available command names. This property is read-only.
  31. *
  32. * @author Qiang Xue <qiang.xue@gmail.com>
  33. * @since 2.0
  34. */
  35. class HelpController extends Controller
  36. {
  37. /**
  38. * Displays available commands or the detailed information
  39. * about a particular command.
  40. *
  41. * @param string $command The name of the command to show help about.
  42. * If not provided, all available commands will be displayed.
  43. * @return integer the exit status
  44. * @throws Exception if the command for help is unknown
  45. */
  46. public function actionIndex($command = null)
  47. {
  48. if ($command !== null) {
  49. $result = Yii::$app->createController($command);
  50. if ($result === false) {
  51. $name = $this->ansiFormat($command, Console::FG_YELLOW);
  52. throw new Exception("No help for unknown command \"$name\".");
  53. }
  54. list($controller, $actionID) = $result;
  55. $actions = $this->getActions($controller);
  56. if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
  57. $this->getSubCommandHelp($controller, $actionID);
  58. } else {
  59. $this->getCommandHelp($controller);
  60. }
  61. } else {
  62. $this->getDefaultHelp();
  63. }
  64. }
  65. /**
  66. * Returns all available command names.
  67. * @return array all available command names
  68. */
  69. public function getCommands()
  70. {
  71. $commands = $this->getModuleCommands(Yii::$app);
  72. sort($commands);
  73. return array_unique($commands);
  74. }
  75. /**
  76. * Returns an array of commands an their descriptions.
  77. * @return array all available commands as keys and their description as values.
  78. */
  79. protected function getCommandDescriptions()
  80. {
  81. $descriptions = [];
  82. foreach ($this->getCommands() as $command) {
  83. $description = '';
  84. $result = Yii::$app->createController($command);
  85. if ($result !== false) {
  86. list($controller, $actionID) = $result;
  87. /** @var Controller $controller */
  88. $description = $controller->getHelpSummary();
  89. }
  90. $descriptions[$command] = $description;
  91. }
  92. return $descriptions;
  93. }
  94. /**
  95. * Returns all available actions of the specified controller.
  96. * @param Controller $controller the controller instance
  97. * @return array all available action IDs.
  98. */
  99. public function getActions($controller)
  100. {
  101. $actions = array_keys($controller->actions());
  102. $class = new \ReflectionClass($controller);
  103. foreach ($class->getMethods() as $method) {
  104. $name = $method->getName();
  105. if ($name !== 'actions' && $method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0) {
  106. $actions[] = Inflector::camel2id(substr($name, 6), '-', true);
  107. }
  108. }
  109. sort($actions);
  110. return array_unique($actions);
  111. }
  112. /**
  113. * Returns available commands of a specified module.
  114. * @param \yii\base\Module $module the module instance
  115. * @return array the available command names
  116. */
  117. protected function getModuleCommands($module)
  118. {
  119. $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/';
  120. $commands = [];
  121. foreach (array_keys($module->controllerMap) as $id) {
  122. $commands[] = $prefix . $id;
  123. }
  124. foreach ($module->getModules() as $id => $child) {
  125. if (($child = $module->getModule($id)) === null) {
  126. continue;
  127. }
  128. foreach ($this->getModuleCommands($child) as $command) {
  129. $commands[] = $command;
  130. }
  131. }
  132. $controllerPath = $module->getControllerPath();
  133. if (is_dir($controllerPath)) {
  134. $files = scandir($controllerPath);
  135. foreach ($files as $file) {
  136. if (!empty($file) && substr_compare($file, 'Controller.php', -14, 14) === 0) {
  137. $controllerClass = $module->controllerNamespace . '\\' . substr(basename($file), 0, -4);
  138. if ($this->validateControllerClass($controllerClass)) {
  139. $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14));
  140. }
  141. }
  142. }
  143. }
  144. return $commands;
  145. }
  146. /**
  147. * Validates if the given class is a valid console controller class.
  148. * @param string $controllerClass
  149. * @return bool
  150. */
  151. protected function validateControllerClass($controllerClass)
  152. {
  153. if (class_exists($controllerClass)) {
  154. $class = new \ReflectionClass($controllerClass);
  155. return !$class->isAbstract() && $class->isSubclassOf('yii\console\Controller');
  156. } else {
  157. return false;
  158. }
  159. }
  160. /**
  161. * Displays all available commands.
  162. */
  163. protected function getDefaultHelp()
  164. {
  165. $commands = $this->getCommandDescriptions();
  166. $this->stdout("\nThis is Yii version " . \Yii::getVersion() . ".\n");
  167. if (!empty($commands)) {
  168. $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
  169. $len = 0;
  170. foreach ($commands as $command => $description) {
  171. if (($l = strlen($command)) > $len) {
  172. $len = $l;
  173. }
  174. }
  175. foreach ($commands as $command => $description) {
  176. $this->stdout("- " . $this->ansiFormat($command, Console::FG_YELLOW));
  177. $this->stdout(str_repeat(' ', $len + 3 - strlen($command)) . $description);
  178. $this->stdout("\n");
  179. }
  180. $scriptName = $this->getScriptName();
  181. $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
  182. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  183. . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
  184. } else {
  185. $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
  186. }
  187. }
  188. /**
  189. * Displays the overall information of the command.
  190. * @param Controller $controller the controller instance
  191. */
  192. protected function getCommandHelp($controller)
  193. {
  194. $controller->color = $this->color;
  195. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  196. $comment = $controller->getHelp();
  197. if ($comment !== '') {
  198. $this->stdout("\n$comment\n\n");
  199. }
  200. $actions = $this->getActions($controller);
  201. if (!empty($actions)) {
  202. $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
  203. $prefix = $controller->getUniqueId();
  204. foreach ($actions as $action) {
  205. $this->stdout('- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW));
  206. if ($action === $controller->defaultAction) {
  207. $this->stdout(' (default)', Console::FG_GREEN);
  208. }
  209. $summary = $controller->getActionHelpSummary($controller->createAction($action));
  210. if ($summary !== '') {
  211. $this->stdout(': ' . $summary);
  212. }
  213. $this->stdout("\n");
  214. }
  215. $scriptName = $this->getScriptName();
  216. $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
  217. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  218. . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
  219. }
  220. }
  221. /**
  222. * Displays the detailed information of a command action.
  223. * @param Controller $controller the controller instance
  224. * @param string $actionID action ID
  225. * @throws Exception if the action does not exist
  226. */
  227. protected function getSubCommandHelp($controller, $actionID)
  228. {
  229. $action = $controller->createAction($actionID);
  230. if ($action === null) {
  231. $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
  232. throw new Exception("No help for unknown sub-command \"$name\".");
  233. }
  234. $description = $controller->getActionHelp($action);
  235. if ($description != '') {
  236. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  237. $this->stdout("\n$description\n\n");
  238. }
  239. $this->stdout("\nUSAGE\n\n", Console::BOLD);
  240. $scriptName = $this->getScriptName();
  241. if ($action->id === $controller->defaultAction) {
  242. $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
  243. } else {
  244. $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
  245. }
  246. $args = $controller->getActionArgsHelp($action);
  247. foreach ($args as $name => $arg) {
  248. if ($arg['required']) {
  249. $this->stdout(' <' . $name . '>', Console::FG_CYAN);
  250. } else {
  251. $this->stdout(' [' . $name . ']', Console::FG_CYAN);
  252. }
  253. }
  254. $options = $controller->getActionOptionsHelp($action);
  255. $options[\yii\console\Application::OPTION_APPCONFIG] = [
  256. 'type' => 'string',
  257. 'default' => null,
  258. 'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
  259. ];
  260. ksort($options);
  261. if (!empty($options)) {
  262. $this->stdout(' [...options...]', Console::FG_RED);
  263. }
  264. $this->stdout("\n\n");
  265. if (!empty($args)) {
  266. foreach ($args as $name => $arg) {
  267. $this->stdout($this->formatOptionHelp(
  268. '- ' . $this->ansiFormat($name, Console::FG_CYAN),
  269. $arg['required'],
  270. $arg['type'],
  271. $arg['default'],
  272. $arg['comment']) . "\n\n");
  273. }
  274. }
  275. if (!empty($options)) {
  276. $this->stdout("\nOPTIONS\n\n", Console::BOLD);
  277. foreach ($options as $name => $option) {
  278. $this->stdout($this->formatOptionHelp(
  279. $this->ansiFormat('--' . $name, Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
  280. !empty($option['required']),
  281. $option['type'],
  282. $option['default'],
  283. $option['comment']) . "\n\n");
  284. }
  285. }
  286. }
  287. /**
  288. * Generates a well-formed string for an argument or option.
  289. * @param string $name the name of the argument or option
  290. * @param boolean $required whether the argument is required
  291. * @param string $type the type of the option or argument
  292. * @param mixed $defaultValue the default value of the option or argument
  293. * @param string $comment comment about the option or argument
  294. * @return string the formatted string for the argument or option
  295. */
  296. protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
  297. {
  298. $comment = trim($comment);
  299. $type = trim($type);
  300. if (strncmp($type, 'bool', 4) === 0) {
  301. $type = 'boolean, 0 or 1';
  302. }
  303. if ($defaultValue !== null && !is_array($defaultValue)) {
  304. if ($type === null) {
  305. $type = gettype($defaultValue);
  306. }
  307. if (is_bool($defaultValue)) {
  308. // show as integer to avoid confusion
  309. $defaultValue = (int) $defaultValue;
  310. }
  311. if (is_string($defaultValue)) {
  312. $defaultValue = "'" . $defaultValue . "'";
  313. } else {
  314. $defaultValue = var_export($defaultValue, true);
  315. }
  316. $doc = "$type (defaults to " . $defaultValue . ")";
  317. } else {
  318. $doc = $type;
  319. }
  320. if ($doc === '') {
  321. $doc = $comment;
  322. } elseif ($comment !== '') {
  323. $doc .= "\n" . preg_replace("/^/m", " ", $comment);
  324. }
  325. $name = $required ? "$name (required)" : $name;
  326. return $doc === '' ? $name : "$name: $doc";
  327. }
  328. /**
  329. * @return string the name of the cli script currently running.
  330. */
  331. protected function getScriptName()
  332. {
  333. return basename(Yii::$app->request->scriptFile);
  334. }
  335. }