Descriptor.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: yunwuxin <448901948@qq.com>
  10. // +----------------------------------------------------------------------
  11. namespace think\console\output;
  12. use think\Console;
  13. use think\console\Command;
  14. use think\console\input\Argument as InputArgument;
  15. use think\console\input\Definition as InputDefinition;
  16. use think\console\input\Option as InputOption;
  17. use think\console\Output;
  18. use think\console\output\descriptor\Console as ConsoleDescription;
  19. class Descriptor
  20. {
  21. /**
  22. * @var Output
  23. */
  24. protected $output;
  25. /**
  26. * {@inheritdoc}
  27. */
  28. public function describe(Output $output, $object, array $options = [])
  29. {
  30. $this->output = $output;
  31. switch (true) {
  32. case $object instanceof InputArgument:
  33. $this->describeInputArgument($object, $options);
  34. break;
  35. case $object instanceof InputOption:
  36. $this->describeInputOption($object, $options);
  37. break;
  38. case $object instanceof InputDefinition:
  39. $this->describeInputDefinition($object, $options);
  40. break;
  41. case $object instanceof Command:
  42. $this->describeCommand($object, $options);
  43. break;
  44. case $object instanceof Console:
  45. $this->describeConsole($object, $options);
  46. break;
  47. default:
  48. throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
  49. }
  50. }
  51. /**
  52. * 输出内容
  53. * @param string $content
  54. * @param bool $decorated
  55. */
  56. protected function write($content, $decorated = false)
  57. {
  58. $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW);
  59. }
  60. /**
  61. * 描述参数
  62. * @param InputArgument $argument
  63. * @param array $options
  64. * @return string|mixed
  65. */
  66. protected function describeInputArgument(InputArgument $argument, array $options = [])
  67. {
  68. if (null !== $argument->getDefault()
  69. && (!is_array($argument->getDefault())
  70. || count($argument->getDefault()))
  71. ) {
  72. $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
  73. } else {
  74. $default = '';
  75. }
  76. $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName());
  77. $spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
  78. $this->writeText(sprintf(" <info>%s</info>%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + <info> + </info> + 2 spaces
  79. preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options);
  80. }
  81. /**
  82. * 描述选项
  83. * @param InputOption $option
  84. * @param array $options
  85. * @return string|mixed
  86. */
  87. protected function describeInputOption(InputOption $option, array $options = [])
  88. {
  89. if ($option->acceptValue() && null !== $option->getDefault()
  90. && (!is_array($option->getDefault())
  91. || count($option->getDefault()))
  92. ) {
  93. $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
  94. } else {
  95. $default = '';
  96. }
  97. $value = '';
  98. if ($option->acceptValue()) {
  99. $value = '=' . strtoupper($option->getName());
  100. if ($option->isValueOptional()) {
  101. $value = '[' . $value . ']';
  102. }
  103. }
  104. $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
  105. $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value));
  106. $spacingWidth = $totalWidth - strlen($synopsis) + 2;
  107. $this->writeText(sprintf(" <info>%s</info>%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + <info> + </info> + 2 spaces
  108. preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''), $options);
  109. }
  110. /**
  111. * 描述输入
  112. * @param InputDefinition $definition
  113. * @param array $options
  114. * @return string|mixed
  115. */
  116. protected function describeInputDefinition(InputDefinition $definition, array $options = [])
  117. {
  118. $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
  119. foreach ($definition->getArguments() as $argument) {
  120. $totalWidth = max($totalWidth, strlen($argument->getName()));
  121. }
  122. if ($definition->getArguments()) {
  123. $this->writeText('<comment>Arguments:</comment>', $options);
  124. $this->writeText("\n");
  125. foreach ($definition->getArguments() as $argument) {
  126. $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
  127. $this->writeText("\n");
  128. }
  129. }
  130. if ($definition->getArguments() && $definition->getOptions()) {
  131. $this->writeText("\n");
  132. }
  133. if ($definition->getOptions()) {
  134. $laterOptions = [];
  135. $this->writeText('<comment>Options:</comment>', $options);
  136. foreach ($definition->getOptions() as $option) {
  137. if (strlen($option->getShortcut()) > 1) {
  138. $laterOptions[] = $option;
  139. continue;
  140. }
  141. $this->writeText("\n");
  142. $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
  143. }
  144. foreach ($laterOptions as $option) {
  145. $this->writeText("\n");
  146. $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
  147. }
  148. }
  149. }
  150. /**
  151. * 描述指令
  152. * @param Command $command
  153. * @param array $options
  154. * @return string|mixed
  155. */
  156. protected function describeCommand(Command $command, array $options = [])
  157. {
  158. $command->getSynopsis(true);
  159. $command->getSynopsis(false);
  160. $command->mergeConsoleDefinition(false);
  161. $this->writeText('<comment>Usage:</comment>', $options);
  162. foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
  163. $this->writeText("\n");
  164. $this->writeText(' ' . $usage, $options);
  165. }
  166. $this->writeText("\n");
  167. $definition = $command->getNativeDefinition();
  168. if ($definition->getOptions() || $definition->getArguments()) {
  169. $this->writeText("\n");
  170. $this->describeInputDefinition($definition, $options);
  171. $this->writeText("\n");
  172. }
  173. if ($help = $command->getProcessedHelp()) {
  174. $this->writeText("\n");
  175. $this->writeText('<comment>Help:</comment>', $options);
  176. $this->writeText("\n");
  177. $this->writeText(' ' . str_replace("\n", "\n ", $help), $options);
  178. $this->writeText("\n");
  179. }
  180. }
  181. /**
  182. * 描述控制台
  183. * @param Console $console
  184. * @param array $options
  185. * @return string|mixed
  186. */
  187. protected function describeConsole(Console $console, array $options = [])
  188. {
  189. $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
  190. $description = new ConsoleDescription($console, $describedNamespace);
  191. if (isset($options['raw_text']) && $options['raw_text']) {
  192. $width = $this->getColumnWidth($description->getCommands());
  193. foreach ($description->getCommands() as $command) {
  194. $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options);
  195. $this->writeText("\n");
  196. }
  197. } else {
  198. if ('' != $help = $console->getHelp()) {
  199. $this->writeText("$help\n\n", $options);
  200. }
  201. $this->writeText("<comment>Usage:</comment>\n", $options);
  202. $this->writeText(" command [options] [arguments]\n\n", $options);
  203. $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options);
  204. $this->writeText("\n");
  205. $this->writeText("\n");
  206. $width = $this->getColumnWidth($description->getCommands());
  207. if ($describedNamespace) {
  208. $this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
  209. } else {
  210. $this->writeText('<comment>Available commands:</comment>', $options);
  211. }
  212. // add commands by namespace
  213. foreach ($description->getNamespaces() as $namespace) {
  214. if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
  215. $this->writeText("\n");
  216. $this->writeText(' <comment>' . $namespace['id'] . '</comment>', $options);
  217. }
  218. foreach ($namespace['commands'] as $name) {
  219. $this->writeText("\n");
  220. $spacingWidth = $width - strlen($name);
  221. $this->writeText(sprintf(" <info>%s</info>%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)
  222. ->getDescription()), $options);
  223. }
  224. }
  225. $this->writeText("\n");
  226. }
  227. }
  228. /**
  229. * {@inheritdoc}
  230. */
  231. private function writeText($content, array $options = [])
  232. {
  233. $this->write(isset($options['raw_text'])
  234. && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true);
  235. }
  236. /**
  237. * 格式化
  238. * @param mixed $default
  239. * @return string
  240. */
  241. private function formatDefaultValue($default)
  242. {
  243. return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  244. }
  245. /**
  246. * @param Command[] $commands
  247. * @return int
  248. */
  249. private function getColumnWidth(array $commands)
  250. {
  251. $width = 0;
  252. foreach ($commands as $command) {
  253. $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width;
  254. }
  255. return $width + 2;
  256. }
  257. /**
  258. * @param InputOption[] $options
  259. * @return int
  260. */
  261. private function calculateTotalWidthForOptions($options)
  262. {
  263. $totalWidth = 0;
  264. foreach ($options as $option) {
  265. $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
  266. if ($option->acceptValue()) {
  267. $valueLength = 1 + strlen($option->getName()); // = + value
  268. $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
  269. $nameLength += $valueLength;
  270. }
  271. $totalWidth = max($totalWidth, $nameLength);
  272. }
  273. return $totalWidth;
  274. }
  275. }