HelpController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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\base\InlineAction;
  11. use yii\console\Controller;
  12. use yii\console\Exception;
  13. use yii\helpers\Console;
  14. use yii\helpers\Inflector;
  15. /**
  16. * This command provides help information about console commands.
  17. *
  18. * This command displays the available command list in
  19. * the application or the detailed instructions about using
  20. * a specific command.
  21. *
  22. * This command can be used as follows on command line:
  23. *
  24. * ~~~
  25. * yii help [command name]
  26. * ~~~
  27. *
  28. * In the above, if the command name is not provided, all
  29. * available commands will be displayed.
  30. *
  31. * @property array $commands All available command names. This property is read-only.
  32. *
  33. * @author Qiang Xue <[email protected]>
  34. * @since 2.0
  35. */
  36. class HelpController extends Controller
  37. {
  38. /**
  39. * Displays available commands or the detailed information
  40. * about a particular command. For example,
  41. *
  42. * @param string $command The name of the command to show help about.
  43. * If not provided, all available commands will be displayed.
  44. * @return integer the exit status
  45. * @throws Exception if the command for help is unknown
  46. */
  47. public function actionIndex($command = null)
  48. {
  49. if ($command !== null) {
  50. $result = Yii::$app->createController($command);
  51. if ($result === false) {
  52. throw new Exception(Yii::t('yii', 'No help for unknown command "{command}".', [
  53. 'command' => $this->ansiFormat($command, Console::FG_YELLOW),
  54. ]));
  55. }
  56. list($controller, $actionID) = $result;
  57. $actions = $this->getActions($controller);
  58. if ($actionID !== '' || count($actions) === 1 && $actions[0] === $controller->defaultAction) {
  59. $this->getActionHelp($controller, $actionID);
  60. } else {
  61. $this->getControllerHelp($controller);
  62. }
  63. } else {
  64. $this->getHelp();
  65. }
  66. }
  67. /**
  68. * Returns all available command names.
  69. * @return array all available command names
  70. */
  71. public function getCommands()
  72. {
  73. $commands = $this->getModuleCommands(Yii::$app);
  74. sort($commands);
  75. return array_unique($commands);
  76. }
  77. /**
  78. * Returns all available actions of the specified controller.
  79. * @param Controller $controller the controller instance
  80. * @return array all available action IDs.
  81. */
  82. public function getActions($controller)
  83. {
  84. $actions = array_keys($controller->actions());
  85. $class = new \ReflectionClass($controller);
  86. foreach ($class->getMethods() as $method) {
  87. $name = $method->getName();
  88. if ($method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0 && $name !== 'actions') {
  89. $actions[] = Inflector::camel2id(substr($name, 6));
  90. }
  91. }
  92. sort($actions);
  93. return array_unique($actions);
  94. }
  95. /**
  96. * Returns available commands of a specified module.
  97. * @param \yii\base\Module $module the module instance
  98. * @return array the available command names
  99. */
  100. protected function getModuleCommands($module)
  101. {
  102. $prefix = $module instanceof Application ? '' : $module->getUniqueID() . '/';
  103. $commands = [];
  104. foreach (array_keys($module->controllerMap) as $id) {
  105. $commands[] = $prefix . $id;
  106. }
  107. foreach ($module->getModules() as $id => $child) {
  108. if (($child = $module->getModule($id)) === null) {
  109. continue;
  110. }
  111. foreach ($this->getModuleCommands($child) as $command) {
  112. $commands[] = $command;
  113. }
  114. }
  115. $controllerPath = $module->getControllerPath();
  116. if (is_dir($controllerPath)) {
  117. $files = scandir($controllerPath);
  118. foreach ($files as $file) {
  119. if (strcmp(substr($file, -14), 'Controller.php') === 0) {
  120. $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14));
  121. }
  122. }
  123. }
  124. return $commands;
  125. }
  126. /**
  127. * Displays all available commands.
  128. */
  129. protected function getHelp()
  130. {
  131. $commands = $this->getCommands();
  132. if (!empty($commands)) {
  133. $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
  134. foreach ($commands as $command) {
  135. echo "- " . $this->ansiFormat($command, Console::FG_YELLOW) . "\n";
  136. }
  137. $scriptName = $this->getScriptName();
  138. $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
  139. echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  140. . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n";
  141. } else {
  142. $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
  143. }
  144. }
  145. /**
  146. * Displays the overall information of the command.
  147. * @param Controller $controller the controller instance
  148. */
  149. protected function getControllerHelp($controller)
  150. {
  151. $class = new \ReflectionClass($controller);
  152. $comment = strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($class->getDocComment(), '/'))), "\r", '');
  153. if (preg_match('/^\s*@\w+/m', $comment, $matches, PREG_OFFSET_CAPTURE)) {
  154. $comment = trim(substr($comment, 0, $matches[0][1]));
  155. }
  156. if ($comment !== '') {
  157. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  158. echo "\n" . Console::renderColoredString($comment) . "\n\n";
  159. }
  160. $actions = $this->getActions($controller);
  161. if (!empty($actions)) {
  162. $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
  163. $prefix = $controller->getUniqueId();
  164. foreach ($actions as $action) {
  165. echo '- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW);
  166. if ($action === $controller->defaultAction) {
  167. $this->stdout(' (default)', Console::FG_GREEN);
  168. }
  169. $summary = $this->getActionSummary($controller, $action);
  170. if ($summary !== '') {
  171. echo ': ' . $summary;
  172. }
  173. echo "\n";
  174. }
  175. $scriptName = $this->getScriptName();
  176. echo "\nTo see the detailed information about individual sub-commands, enter:\n";
  177. echo "\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  178. . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n";
  179. }
  180. }
  181. /**
  182. * Returns the short summary of the action.
  183. * @param Controller $controller the controller instance
  184. * @param string $actionID action ID
  185. * @return string the summary about the action
  186. */
  187. protected function getActionSummary($controller, $actionID)
  188. {
  189. $action = $controller->createAction($actionID);
  190. if ($action === null) {
  191. return '';
  192. }
  193. if ($action instanceof InlineAction) {
  194. $reflection = new \ReflectionMethod($controller, $action->actionMethod);
  195. } else {
  196. $reflection = new \ReflectionClass($action);
  197. }
  198. $tags = $this->parseComment($reflection->getDocComment());
  199. if ($tags['description'] !== '') {
  200. $limit = 73 - strlen($action->getUniqueId());
  201. if ($actionID === $controller->defaultAction) {
  202. $limit -= 10;
  203. }
  204. if ($limit < 0) {
  205. $limit = 50;
  206. }
  207. $description = $tags['description'];
  208. if (($pos = strpos($tags['description'], "\n")) !== false) {
  209. $description = substr($description, 0, $pos);
  210. }
  211. $text = substr($description, 0, $limit);
  212. return strlen($description) > $limit ? $text . '...' : $text;
  213. } else {
  214. return '';
  215. }
  216. }
  217. /**
  218. * Displays the detailed information of a command action.
  219. * @param Controller $controller the controller instance
  220. * @param string $actionID action ID
  221. * @throws Exception if the action does not exist
  222. */
  223. protected function getActionHelp($controller, $actionID)
  224. {
  225. $action = $controller->createAction($actionID);
  226. if ($action === null) {
  227. throw new Exception(Yii::t('yii', 'No help for unknown sub-command "{command}".', [
  228. 'command' => rtrim($controller->getUniqueId() . '/' . $actionID, '/'),
  229. ]));
  230. }
  231. if ($action instanceof InlineAction) {
  232. $method = new \ReflectionMethod($controller, $action->actionMethod);
  233. } else {
  234. $method = new \ReflectionMethod($action, 'run');
  235. }
  236. $tags = $this->parseComment($method->getDocComment());
  237. $options = $this->getOptionHelps($controller);
  238. if ($tags['description'] !== '') {
  239. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  240. echo "\n" . Console::renderColoredString($tags['description']) . "\n\n";
  241. }
  242. $this->stdout("\nUSAGE\n\n", Console::BOLD);
  243. $scriptName = $this->getScriptName();
  244. if ($action->id === $controller->defaultAction) {
  245. echo $scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW);
  246. } else {
  247. echo $scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW);
  248. }
  249. list ($required, $optional) = $this->getArgHelps($method, isset($tags['param']) ? $tags['param'] : []);
  250. foreach ($required as $arg => $description) {
  251. $this->stdout(' <' . $arg . '>', Console::FG_CYAN);
  252. }
  253. foreach ($optional as $arg => $description) {
  254. $this->stdout(' [' . $arg . ']', Console::FG_CYAN);
  255. }
  256. if (!empty($options)) {
  257. $this->stdout(' [...options...]', Console::FG_RED);
  258. }
  259. echo "\n\n";
  260. if (!empty($required) || !empty($optional)) {
  261. echo implode("\n\n", array_merge($required, $optional)) . "\n\n";
  262. }
  263. $options = $this->getOptionHelps($controller);
  264. if (!empty($options)) {
  265. $this->stdout("\nOPTIONS\n\n", Console::BOLD);
  266. echo implode("\n\n", $options) . "\n\n";
  267. }
  268. }
  269. /**
  270. * Returns the help information about arguments.
  271. * @param \ReflectionMethod $method
  272. * @param string $tags the parsed comment block related with arguments
  273. * @return array the required and optional argument help information
  274. */
  275. protected function getArgHelps($method, $tags)
  276. {
  277. if (is_string($tags)) {
  278. $tags = [$tags];
  279. }
  280. $params = $method->getParameters();
  281. $optional = $required = [];
  282. foreach ($params as $i => $param) {
  283. $name = $param->getName();
  284. $tag = isset($tags[$i]) ? $tags[$i] : '';
  285. if (preg_match('/^([^\s]+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
  286. $type = $matches[1];
  287. $comment = $matches[3];
  288. } else {
  289. $type = null;
  290. $comment = $tag;
  291. }
  292. if ($param->isDefaultValueAvailable()) {
  293. $optional[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), false, $type, $param->getDefaultValue(), $comment);
  294. } else {
  295. $required[$name] = $this->formatOptionHelp('- ' . $this->ansiFormat($name, Console::FG_CYAN), true, $type, null, $comment);
  296. }
  297. }
  298. return [$required, $optional];
  299. }
  300. /**
  301. * Returns the help information about the options available for a console controller.
  302. * @param Controller $controller the console controller
  303. * @return array the help information about the options
  304. */
  305. protected function getOptionHelps($controller)
  306. {
  307. $optionNames = $controller->globalOptions();
  308. if (empty($optionNames)) {
  309. return [];
  310. }
  311. $class = new \ReflectionClass($controller);
  312. $options = [];
  313. foreach ($class->getProperties() as $property) {
  314. $name = $property->getName();
  315. if (!in_array($name, $optionNames, true)) {
  316. continue;
  317. }
  318. $defaultValue = $property->getValue($controller);
  319. $tags = $this->parseComment($property->getDocComment());
  320. if (isset($tags['var']) || isset($tags['property'])) {
  321. $doc = isset($tags['var']) ? $tags['var'] : $tags['property'];
  322. if (is_array($doc)) {
  323. $doc = reset($doc);
  324. }
  325. if (preg_match('/^([^\s]+)(.*)/s', $doc, $matches)) {
  326. $type = $matches[1];
  327. $comment = $matches[2];
  328. } else {
  329. $type = null;
  330. $comment = $doc;
  331. }
  332. $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, $type, $defaultValue, $comment);
  333. } else {
  334. $options[$name] = $this->formatOptionHelp($this->ansiFormat('--' . $name, Console::FG_RED), false, null, $defaultValue, '');
  335. }
  336. }
  337. ksort($options);
  338. return $options;
  339. }
  340. /**
  341. * Parses the comment block into tags.
  342. * @param string $comment the comment block
  343. * @return array the parsed tags
  344. */
  345. protected function parseComment($comment)
  346. {
  347. $tags = [];
  348. $comment = "@description \n" . strtr(trim(preg_replace('/^\s*\**( |\t)?/m', '', trim($comment, '/'))), "\r", '');
  349. $parts = preg_split('/^\s*@/m', $comment, -1, PREG_SPLIT_NO_EMPTY);
  350. foreach ($parts as $part) {
  351. if (preg_match('/^(\w+)(.*)/ms', trim($part), $matches)) {
  352. $name = $matches[1];
  353. if (!isset($tags[$name])) {
  354. $tags[$name] = trim($matches[2]);
  355. } elseif (is_array($tags[$name])) {
  356. $tags[$name][] = trim($matches[2]);
  357. } else {
  358. $tags[$name] = [$tags[$name], trim($matches[2])];
  359. }
  360. }
  361. }
  362. return $tags;
  363. }
  364. /**
  365. * Generates a well-formed string for an argument or option.
  366. * @param string $name the name of the argument or option
  367. * @param boolean $required whether the argument is required
  368. * @param string $type the type of the option or argument
  369. * @param mixed $defaultValue the default value of the option or argument
  370. * @param string $comment comment about the option or argument
  371. * @return string the formatted string for the argument or option
  372. */
  373. protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
  374. {
  375. $doc = '';
  376. $comment = trim($comment);
  377. if ($defaultValue !== null && !is_array($defaultValue)) {
  378. if ($type === null) {
  379. $type = gettype($defaultValue);
  380. }
  381. if (is_bool($defaultValue)) {
  382. // show as integer to avoid confusion
  383. $defaultValue = (int)$defaultValue;
  384. }
  385. $doc = "$type (defaults to " . var_export($defaultValue, true) . ")";
  386. } elseif (trim($type) !== '') {
  387. $doc = $type;
  388. }
  389. if ($doc === '') {
  390. $doc = $comment;
  391. } elseif ($comment !== '') {
  392. $doc .= "\n" . preg_replace("/^/m", " ", $comment);
  393. }
  394. $name = $required ? "$name (required)" : $name;
  395. return $doc === '' ? $name : "$name: $doc";
  396. }
  397. /**
  398. * @return string the name of the cli script currently running.
  399. */
  400. protected function getScriptName()
  401. {
  402. return basename(Yii::$app->request->scriptFile);
  403. }
  404. }