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.

441 lines
16KB

  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 boolean
  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. $result = Yii::$app->createController($command);
  172. if ($result !== false) {
  173. /** @var $controller Controller */
  174. list($controller, $actionID) = $result;
  175. $actions = $this->getActions($controller);
  176. if (!empty($actions)) {
  177. $prefix = $controller->getUniqueId();
  178. foreach ($actions as $action) {
  179. $string = $prefix . '/' . $action;
  180. if ($action === $controller->defaultAction) {
  181. $string .= ' (default)';
  182. }
  183. if (($l = strlen($string)) > $len) {
  184. $len = $l;
  185. }
  186. }
  187. }
  188. } elseif (($l = strlen($command)) > $len) {
  189. $len = $l;
  190. }
  191. }
  192. foreach ($commands as $command => $description) {
  193. $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
  194. $this->stdout(str_repeat(' ', $len + 4 - strlen($command)));
  195. $this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD);
  196. $this->stdout("\n");
  197. $result = Yii::$app->createController($command);
  198. if ($result !== false) {
  199. list($controller, $actionID) = $result;
  200. $actions = $this->getActions($controller);
  201. if (!empty($actions)) {
  202. $prefix = $controller->getUniqueId();
  203. foreach ($actions as $action) {
  204. $string = ' ' . $prefix . '/' . $action;
  205. $this->stdout(' ' . $this->ansiFormat($string, Console::FG_GREEN));
  206. if ($action === $controller->defaultAction) {
  207. $string .= ' (default)';
  208. $this->stdout(' (default)', Console::FG_YELLOW);
  209. }
  210. $summary = $controller->getActionHelpSummary($controller->createAction($action));
  211. if ($summary !== '') {
  212. $this->stdout(str_repeat(' ', $len + 4 - strlen($string)));
  213. $this->stdout(Console::wrapText($summary, $len + 4 + 2));
  214. }
  215. $this->stdout("\n");
  216. }
  217. }
  218. $this->stdout("\n");
  219. }
  220. }
  221. $scriptName = $this->getScriptName();
  222. $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
  223. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  224. . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
  225. } else {
  226. $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
  227. }
  228. }
  229. /**
  230. * Displays the overall information of the command.
  231. * @param Controller $controller the controller instance
  232. */
  233. protected function getCommandHelp($controller)
  234. {
  235. $controller->color = $this->color;
  236. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  237. $comment = $controller->getHelp();
  238. if ($comment !== '') {
  239. $this->stdout("\n$comment\n\n");
  240. }
  241. $actions = $this->getActions($controller);
  242. if (!empty($actions)) {
  243. $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
  244. $prefix = $controller->getUniqueId();
  245. $maxlen = 5;
  246. foreach ($actions as $action) {
  247. $len = strlen($prefix.'/'.$action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
  248. if ($maxlen < $len) {
  249. $maxlen = $len;
  250. }
  251. }
  252. foreach ($actions as $action) {
  253. $this->stdout('- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW));
  254. $len = strlen($prefix.'/'.$action) + 2;
  255. if ($action === $controller->defaultAction) {
  256. $this->stdout(' (default)', Console::FG_GREEN);
  257. $len += 10;
  258. }
  259. $summary = $controller->getActionHelpSummary($controller->createAction($action));
  260. if ($summary !== '') {
  261. $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
  262. }
  263. $this->stdout("\n");
  264. }
  265. $scriptName = $this->getScriptName();
  266. $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
  267. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  268. . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
  269. }
  270. }
  271. /**
  272. * Displays the detailed information of a command action.
  273. * @param Controller $controller the controller instance
  274. * @param string $actionID action ID
  275. * @throws Exception if the action does not exist
  276. */
  277. protected function getSubCommandHelp($controller, $actionID)
  278. {
  279. $action = $controller->createAction($actionID);
  280. if ($action === null) {
  281. $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
  282. throw new Exception("No help for unknown sub-command \"$name\".");
  283. }
  284. $description = $controller->getActionHelp($action);
  285. if ($description !== '') {
  286. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  287. $this->stdout("\n$description\n\n");
  288. }
  289. $this->stdout("\nUSAGE\n\n", Console::BOLD);
  290. $scriptName = $this->getScriptName();
  291. if ($action->id === $controller->defaultAction) {
  292. $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
  293. } else {
  294. $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
  295. }
  296. $args = $controller->getActionArgsHelp($action);
  297. foreach ($args as $name => $arg) {
  298. if ($arg['required']) {
  299. $this->stdout(' <' . $name . '>', Console::FG_CYAN);
  300. } else {
  301. $this->stdout(' [' . $name . ']', Console::FG_CYAN);
  302. }
  303. }
  304. $options = $controller->getActionOptionsHelp($action);
  305. $options[\yii\console\Application::OPTION_APPCONFIG] = [
  306. 'type' => 'string',
  307. 'default' => null,
  308. 'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
  309. ];
  310. ksort($options);
  311. if (!empty($options)) {
  312. $this->stdout(' [...options...]', Console::FG_RED);
  313. }
  314. $this->stdout("\n\n");
  315. if (!empty($args)) {
  316. foreach ($args as $name => $arg) {
  317. $this->stdout($this->formatOptionHelp(
  318. '- ' . $this->ansiFormat($name, Console::FG_CYAN),
  319. $arg['required'],
  320. $arg['type'],
  321. $arg['default'],
  322. $arg['comment']) . "\n\n");
  323. }
  324. }
  325. if (!empty($options)) {
  326. $this->stdout("\nOPTIONS\n\n", Console::BOLD);
  327. foreach ($options as $name => $option) {
  328. $this->stdout($this->formatOptionHelp(
  329. $this->ansiFormat('--' . $name . $this->formatOptionAliases($controller, $name), Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
  330. !empty($option['required']),
  331. $option['type'],
  332. $option['default'],
  333. $option['comment']) . "\n\n");
  334. }
  335. }
  336. }
  337. /**
  338. * Generates a well-formed string for an argument or option.
  339. * @param string $name the name of the argument or option
  340. * @param boolean $required whether the argument is required
  341. * @param string $type the type of the option or argument
  342. * @param mixed $defaultValue the default value of the option or argument
  343. * @param string $comment comment about the option or argument
  344. * @return string the formatted string for the argument or option
  345. */
  346. protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
  347. {
  348. $comment = trim($comment);
  349. $type = trim($type);
  350. if (strncmp($type, 'bool', 4) === 0) {
  351. $type = 'boolean, 0 or 1';
  352. }
  353. if ($defaultValue !== null && !is_array($defaultValue)) {
  354. if ($type === null) {
  355. $type = gettype($defaultValue);
  356. }
  357. if (is_bool($defaultValue)) {
  358. // show as integer to avoid confusion
  359. $defaultValue = (int) $defaultValue;
  360. }
  361. if (is_string($defaultValue)) {
  362. $defaultValue = "'" . $defaultValue . "'";
  363. } else {
  364. $defaultValue = var_export($defaultValue, true);
  365. }
  366. $doc = "$type (defaults to $defaultValue)";
  367. } else {
  368. $doc = $type;
  369. }
  370. if ($doc === '') {
  371. $doc = $comment;
  372. } elseif ($comment !== '') {
  373. $doc .= "\n" . preg_replace('/^/m', ' ', $comment);
  374. }
  375. $name = $required ? "$name (required)" : $name;
  376. return $doc === '' ? $name : "$name: $doc";
  377. }
  378. /**
  379. * @param Controller $controller the controller instance
  380. * @param string $option the option name
  381. * @return string the formatted string for the alias argument or option
  382. * @since 2.0.8
  383. */
  384. protected function formatOptionAliases($controller, $option)
  385. {
  386. $aliases = $controller->optionAliases();
  387. foreach ($aliases as $name => $value) {
  388. if ($value === $option) {
  389. return ', -' . $name;
  390. }
  391. }
  392. return '';
  393. }
  394. /**
  395. * @return string the name of the cli script currently running.
  396. */
  397. protected function getScriptName()
  398. {
  399. return basename(Yii::$app->request->scriptFile);
  400. }
  401. }