HelpController.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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 int 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. * List all available controllers and actions in machine readable format.
  67. * This is used for shell completion.
  68. * @since 2.0.11
  69. */
  70. public function actionList()
  71. {
  72. $commands = $this->getCommandDescriptions();
  73. foreach ($commands as $command => $description) {
  74. $result = Yii::$app->createController($command);
  75. if ($result === false || !($result[0] instanceof Controller)) {
  76. continue;
  77. }
  78. /** @var $controller Controller */
  79. list($controller, $actionID) = $result;
  80. $actions = $this->getActions($controller);
  81. if (!empty($actions)) {
  82. $prefix = $controller->getUniqueId();
  83. $this->stdout("$prefix\n");
  84. foreach ($actions as $action) {
  85. $this->stdout("$prefix/$action\n");
  86. }
  87. }
  88. }
  89. }
  90. /**
  91. * List all available options for the $action in machine readable format.
  92. * This is used for shell completion.
  93. *
  94. * @param string $action route to action
  95. * @since 2.0.11
  96. */
  97. public function actionListActionOptions($action)
  98. {
  99. $result = Yii::$app->createController($action);
  100. if ($result === false || !($result[0] instanceof Controller)) {
  101. return;
  102. }
  103. /** @var Controller $controller */
  104. list($controller, $actionID) = $result;
  105. $action = $controller->createAction($actionID);
  106. if ($action === null) {
  107. return;
  108. }
  109. $arguments = $controller->getActionArgsHelp($action);
  110. foreach ($arguments as $argument => $help) {
  111. $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument;
  112. $this->stdout($argument . ':' . $description . "\n");
  113. }
  114. $this->stdout("\n");
  115. $options = $controller->getActionOptionsHelp($action);
  116. foreach ($options as $argument => $help) {
  117. $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument;
  118. $this->stdout('--' . $argument . ':' . $description . "\n");
  119. }
  120. }
  121. /**
  122. * Displays usage information for $action
  123. *
  124. * @param string $action route to action
  125. * @since 2.0.11
  126. */
  127. public function actionUsage($action)
  128. {
  129. $result = Yii::$app->createController($action);
  130. if ($result === false || !($result[0] instanceof Controller)) {
  131. return;
  132. }
  133. /** @var Controller $controller */
  134. list($controller, $actionID) = $result;
  135. $action = $controller->createAction($actionID);
  136. if ($action === null) {
  137. return;
  138. }
  139. $scriptName = $this->getScriptName();
  140. if ($action->id === $controller->defaultAction) {
  141. $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
  142. } else {
  143. $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
  144. }
  145. $args = $controller->getActionArgsHelp($action);
  146. foreach ($args as $name => $arg) {
  147. if ($arg['required']) {
  148. $this->stdout(' <' . $name . '>', Console::FG_CYAN);
  149. } else {
  150. $this->stdout(' [' . $name . ']', Console::FG_CYAN);
  151. }
  152. }
  153. $this->stdout("\n");
  154. return;
  155. }
  156. /**
  157. * Returns all available command names.
  158. * @return array all available command names
  159. */
  160. public function getCommands()
  161. {
  162. $commands = $this->getModuleCommands(Yii::$app);
  163. sort($commands);
  164. return array_unique($commands);
  165. }
  166. /**
  167. * Returns an array of commands an their descriptions.
  168. * @return array all available commands as keys and their description as values.
  169. */
  170. protected function getCommandDescriptions()
  171. {
  172. $descriptions = [];
  173. foreach ($this->getCommands() as $command) {
  174. $description = '';
  175. $result = Yii::$app->createController($command);
  176. if ($result !== false && $result[0] instanceof Controller) {
  177. list($controller, $actionID) = $result;
  178. /** @var Controller $controller */
  179. $description = $controller->getHelpSummary();
  180. }
  181. $descriptions[$command] = $description;
  182. }
  183. return $descriptions;
  184. }
  185. /**
  186. * Returns all available actions of the specified controller.
  187. * @param Controller $controller the controller instance
  188. * @return array all available action IDs.
  189. */
  190. public function getActions($controller)
  191. {
  192. $actions = array_keys($controller->actions());
  193. $class = new \ReflectionClass($controller);
  194. foreach ($class->getMethods() as $method) {
  195. $name = $method->getName();
  196. if ($name !== 'actions' && $method->isPublic() && !$method->isStatic() && strpos($name, 'action') === 0) {
  197. $actions[] = Inflector::camel2id(substr($name, 6), '-', true);
  198. }
  199. }
  200. sort($actions);
  201. return array_unique($actions);
  202. }
  203. /**
  204. * Returns available commands of a specified module.
  205. * @param \yii\base\Module $module the module instance
  206. * @return array the available command names
  207. */
  208. protected function getModuleCommands($module)
  209. {
  210. $prefix = $module instanceof Application ? '' : $module->getUniqueId() . '/';
  211. $commands = [];
  212. foreach (array_keys($module->controllerMap) as $id) {
  213. $commands[] = $prefix . $id;
  214. }
  215. foreach ($module->getModules() as $id => $child) {
  216. if (($child = $module->getModule($id)) === null) {
  217. continue;
  218. }
  219. foreach ($this->getModuleCommands($child) as $command) {
  220. $commands[] = $command;
  221. }
  222. }
  223. $controllerPath = $module->getControllerPath();
  224. if (is_dir($controllerPath)) {
  225. $files = scandir($controllerPath);
  226. foreach ($files as $file) {
  227. if (!empty($file) && substr_compare($file, 'Controller.php', -14, 14) === 0) {
  228. $controllerClass = $module->controllerNamespace . '\\' . substr(basename($file), 0, -4);
  229. if ($this->validateControllerClass($controllerClass)) {
  230. $commands[] = $prefix . Inflector::camel2id(substr(basename($file), 0, -14));
  231. }
  232. }
  233. }
  234. }
  235. return $commands;
  236. }
  237. /**
  238. * Validates if the given class is a valid console controller class.
  239. * @param string $controllerClass
  240. * @return bool
  241. */
  242. protected function validateControllerClass($controllerClass)
  243. {
  244. if (class_exists($controllerClass)) {
  245. $class = new \ReflectionClass($controllerClass);
  246. return !$class->isAbstract() && $class->isSubclassOf('yii\console\Controller');
  247. } else {
  248. return false;
  249. }
  250. }
  251. /**
  252. * Displays all available commands.
  253. */
  254. protected function getDefaultHelp()
  255. {
  256. $commands = $this->getCommandDescriptions();
  257. $this->stdout($this->getDefaultHelpHeader());
  258. if (!empty($commands)) {
  259. $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
  260. $len = 0;
  261. foreach ($commands as $command => $description) {
  262. $result = Yii::$app->createController($command);
  263. if ($result !== false && $result[0] instanceof Controller) {
  264. /** @var $controller Controller */
  265. list($controller, $actionID) = $result;
  266. $actions = $this->getActions($controller);
  267. if (!empty($actions)) {
  268. $prefix = $controller->getUniqueId();
  269. foreach ($actions as $action) {
  270. $string = $prefix . '/' . $action;
  271. if ($action === $controller->defaultAction) {
  272. $string .= ' (default)';
  273. }
  274. if (($l = strlen($string)) > $len) {
  275. $len = $l;
  276. }
  277. }
  278. }
  279. } elseif (($l = strlen($command)) > $len) {
  280. $len = $l;
  281. }
  282. }
  283. foreach ($commands as $command => $description) {
  284. $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
  285. $this->stdout(str_repeat(' ', $len + 4 - strlen($command)));
  286. $this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD);
  287. $this->stdout("\n");
  288. $result = Yii::$app->createController($command);
  289. if ($result !== false && $result[0] instanceof Controller) {
  290. list($controller, $actionID) = $result;
  291. $actions = $this->getActions($controller);
  292. if (!empty($actions)) {
  293. $prefix = $controller->getUniqueId();
  294. foreach ($actions as $action) {
  295. $string = ' ' . $prefix . '/' . $action;
  296. $this->stdout(' ' . $this->ansiFormat($string, Console::FG_GREEN));
  297. if ($action === $controller->defaultAction) {
  298. $string .= ' (default)';
  299. $this->stdout(' (default)', Console::FG_YELLOW);
  300. }
  301. $summary = $controller->getActionHelpSummary($controller->createAction($action));
  302. if ($summary !== '') {
  303. $this->stdout(str_repeat(' ', $len + 4 - strlen($string)));
  304. $this->stdout(Console::wrapText($summary, $len + 4 + 2));
  305. }
  306. $this->stdout("\n");
  307. }
  308. }
  309. $this->stdout("\n");
  310. }
  311. }
  312. $scriptName = $this->getScriptName();
  313. $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
  314. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  315. . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
  316. } else {
  317. $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
  318. }
  319. }
  320. /**
  321. * Displays the overall information of the command.
  322. * @param Controller $controller the controller instance
  323. */
  324. protected function getCommandHelp($controller)
  325. {
  326. $controller->color = $this->color;
  327. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  328. $comment = $controller->getHelp();
  329. if ($comment !== '') {
  330. $this->stdout("\n$comment\n\n");
  331. }
  332. $actions = $this->getActions($controller);
  333. if (!empty($actions)) {
  334. $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
  335. $prefix = $controller->getUniqueId();
  336. $maxlen = 5;
  337. foreach ($actions as $action) {
  338. $len = strlen($prefix.'/'.$action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
  339. if ($maxlen < $len) {
  340. $maxlen = $len;
  341. }
  342. }
  343. foreach ($actions as $action) {
  344. $this->stdout('- ' . $this->ansiFormat($prefix.'/'.$action, Console::FG_YELLOW));
  345. $len = strlen($prefix.'/'.$action) + 2;
  346. if ($action === $controller->defaultAction) {
  347. $this->stdout(' (default)', Console::FG_GREEN);
  348. $len += 10;
  349. }
  350. $summary = $controller->getActionHelpSummary($controller->createAction($action));
  351. if ($summary !== '') {
  352. $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
  353. }
  354. $this->stdout("\n");
  355. }
  356. $scriptName = $this->getScriptName();
  357. $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
  358. $this->stdout("\n $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
  359. . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
  360. }
  361. }
  362. /**
  363. * Displays the detailed information of a command action.
  364. * @param Controller $controller the controller instance
  365. * @param string $actionID action ID
  366. * @throws Exception if the action does not exist
  367. */
  368. protected function getSubCommandHelp($controller, $actionID)
  369. {
  370. $action = $controller->createAction($actionID);
  371. if ($action === null) {
  372. $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionID, '/'), Console::FG_YELLOW);
  373. throw new Exception("No help for unknown sub-command \"$name\".");
  374. }
  375. $description = $controller->getActionHelp($action);
  376. if ($description !== '') {
  377. $this->stdout("\nDESCRIPTION\n", Console::BOLD);
  378. $this->stdout("\n$description\n\n");
  379. }
  380. $this->stdout("\nUSAGE\n\n", Console::BOLD);
  381. $scriptName = $this->getScriptName();
  382. if ($action->id === $controller->defaultAction) {
  383. $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
  384. } else {
  385. $this->stdout($scriptName . ' ' . $this->ansiFormat($action->getUniqueId(), Console::FG_YELLOW));
  386. }
  387. $args = $controller->getActionArgsHelp($action);
  388. foreach ($args as $name => $arg) {
  389. if ($arg['required']) {
  390. $this->stdout(' <' . $name . '>', Console::FG_CYAN);
  391. } else {
  392. $this->stdout(' [' . $name . ']', Console::FG_CYAN);
  393. }
  394. }
  395. $options = $controller->getActionOptionsHelp($action);
  396. $options[\yii\console\Application::OPTION_APPCONFIG] = [
  397. 'type' => 'string',
  398. 'default' => null,
  399. 'comment' => "custom application configuration file path.\nIf not set, default application configuration is used.",
  400. ];
  401. ksort($options);
  402. if (!empty($options)) {
  403. $this->stdout(' [...options...]', Console::FG_RED);
  404. }
  405. $this->stdout("\n\n");
  406. if (!empty($args)) {
  407. foreach ($args as $name => $arg) {
  408. $this->stdout($this->formatOptionHelp(
  409. '- ' . $this->ansiFormat($name, Console::FG_CYAN),
  410. $arg['required'],
  411. $arg['type'],
  412. $arg['default'],
  413. $arg['comment']) . "\n\n");
  414. }
  415. }
  416. if (!empty($options)) {
  417. $this->stdout("\nOPTIONS\n\n", Console::BOLD);
  418. foreach ($options as $name => $option) {
  419. $this->stdout($this->formatOptionHelp(
  420. $this->ansiFormat('--' . $name . $this->formatOptionAliases($controller, $name), Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
  421. !empty($option['required']),
  422. $option['type'],
  423. $option['default'],
  424. $option['comment']) . "\n\n");
  425. }
  426. }
  427. }
  428. /**
  429. * Generates a well-formed string for an argument or option.
  430. * @param string $name the name of the argument or option
  431. * @param bool $required whether the argument is required
  432. * @param string $type the type of the option or argument
  433. * @param mixed $defaultValue the default value of the option or argument
  434. * @param string $comment comment about the option or argument
  435. * @return string the formatted string for the argument or option
  436. */
  437. protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
  438. {
  439. $comment = trim($comment);
  440. $type = trim($type);
  441. if (strncmp($type, 'bool', 4) === 0) {
  442. $type = 'boolean, 0 or 1';
  443. }
  444. if ($defaultValue !== null && !is_array($defaultValue)) {
  445. if ($type === null) {
  446. $type = gettype($defaultValue);
  447. }
  448. if (is_bool($defaultValue)) {
  449. // show as integer to avoid confusion
  450. $defaultValue = (int) $defaultValue;
  451. }
  452. if (is_string($defaultValue)) {
  453. $defaultValue = "'" . $defaultValue . "'";
  454. } else {
  455. $defaultValue = var_export($defaultValue, true);
  456. }
  457. $doc = "$type (defaults to $defaultValue)";
  458. } else {
  459. $doc = $type;
  460. }
  461. if ($doc === '') {
  462. $doc = $comment;
  463. } elseif ($comment !== '') {
  464. $doc .= "\n" . preg_replace('/^/m', ' ', $comment);
  465. }
  466. $name = $required ? "$name (required)" : $name;
  467. return $doc === '' ? $name : "$name: $doc";
  468. }
  469. /**
  470. * @param Controller $controller the controller instance
  471. * @param string $option the option name
  472. * @return string the formatted string for the alias argument or option
  473. * @since 2.0.8
  474. */
  475. protected function formatOptionAliases($controller, $option)
  476. {
  477. $aliases = $controller->optionAliases();
  478. foreach ($aliases as $name => $value) {
  479. if ($value === $option) {
  480. return ', -' . $name;
  481. }
  482. }
  483. return '';
  484. }
  485. /**
  486. * @return string the name of the cli script currently running.
  487. */
  488. protected function getScriptName()
  489. {
  490. return basename(Yii::$app->request->scriptFile);
  491. }
  492. /**
  493. * Return a default help header.
  494. * @return string default help header.
  495. * @since 2.0.11
  496. */
  497. protected function getDefaultHelpHeader()
  498. {
  499. return "\nThis is Yii version " . \Yii::getVersion() . ".\n";
  500. }
  501. }