HelpController.php 21 KB

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