CommandRunner.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  12. * @link http://cakephp.org CakePHP(tm) Project
  13. * @since 3.5.0
  14. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Console;
  17. use Cake\Command\HelpCommand;
  18. use Cake\Command\VersionCommand;
  19. use Cake\Console\Exception\StopException;
  20. use Cake\Core\ConsoleApplicationInterface;
  21. use Cake\Core\HttpApplicationInterface;
  22. use Cake\Core\PluginApplicationInterface;
  23. use Cake\Event\EventDispatcherInterface;
  24. use Cake\Event\EventDispatcherTrait;
  25. use Cake\Event\EventManager;
  26. use Cake\Event\EventManagerInterface;
  27. use Cake\Routing\Router;
  28. use Cake\Utility\Inflector;
  29. use InvalidArgumentException;
  30. use RuntimeException;
  31. /**
  32. * Run CLI commands for the provided application.
  33. */
  34. class CommandRunner implements EventDispatcherInterface
  35. {
  36. use EventDispatcherTrait;
  37. /**
  38. * The application console commands are being run for.
  39. *
  40. * @var \Cake\Core\ConsoleApplicationInterface
  41. */
  42. protected $app;
  43. /**
  44. * The application console commands are being run for.
  45. *
  46. * @var \Cake\Console\CommandFactoryInterface
  47. */
  48. protected $factory;
  49. /**
  50. * The root command name. Defaults to `cake`.
  51. *
  52. * @var string
  53. */
  54. protected $root;
  55. /**
  56. * Alias mappings.
  57. *
  58. * @var array
  59. */
  60. protected $aliases = [];
  61. /**
  62. * Constructor
  63. *
  64. * @param \Cake\Core\ConsoleApplicationInterface $app The application to run CLI commands for.
  65. * @param string $root The root command name to be removed from argv.
  66. * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance.
  67. */
  68. public function __construct(
  69. ConsoleApplicationInterface $app,
  70. $root = 'cake',
  71. ?CommandFactoryInterface $factory = null
  72. ) {
  73. $this->app = $app;
  74. $this->root = $root;
  75. $this->factory = $factory ?: new CommandFactory();
  76. $this->aliases = [
  77. '--version' => 'version',
  78. '--help' => 'help',
  79. '-h' => 'help',
  80. ];
  81. }
  82. /**
  83. * Replace the entire alias map for a runner.
  84. *
  85. * Aliases allow you to define alternate names for commands
  86. * in the collection. This can be useful to add top level switches
  87. * like `--version` or `-h`
  88. *
  89. * ### Usage
  90. *
  91. * ```
  92. * $runner->setAliases(['--version' => 'version']);
  93. * ```
  94. *
  95. * @param array $aliases The map of aliases to replace.
  96. * @return $this
  97. */
  98. public function setAliases(array $aliases): self
  99. {
  100. $this->aliases = $aliases;
  101. return $this;
  102. }
  103. /**
  104. * Run the command contained in $argv.
  105. *
  106. * Use the application to do the following:
  107. *
  108. * - Bootstrap the application
  109. * - Create the CommandCollection using the console() hook on the application.
  110. * - Trigger the `Console.buildCommands` event of auto-wiring plugins.
  111. * - Run the requested command.
  112. *
  113. * @param array $argv The arguments from the CLI environment.
  114. * @param \Cake\Console\ConsoleIo|null $io The ConsoleIo instance. Used primarily for testing.
  115. * @return int The exit code of the command.
  116. * @throws \RuntimeException
  117. */
  118. public function run(array $argv, ?ConsoleIo $io = null): int
  119. {
  120. $this->bootstrap();
  121. $commands = new CommandCollection([
  122. 'version' => VersionCommand::class,
  123. 'help' => HelpCommand::class,
  124. ]);
  125. $commands = $this->app->console($commands);
  126. if ($this->app instanceof PluginApplicationInterface) {
  127. $commands = $this->app->pluginConsole($commands);
  128. }
  129. $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
  130. $this->loadRoutes();
  131. if (empty($argv)) {
  132. throw new RuntimeException("Cannot run any commands. No arguments received.");
  133. }
  134. // Remove the root executable segment
  135. array_shift($argv);
  136. $io = $io ?: new ConsoleIo();
  137. [$name, $argv] = $this->longestCommandName($commands, $argv);
  138. $name = $this->resolveName($commands, $io, $name);
  139. $result = Command::CODE_ERROR;
  140. $shell = $this->getShell($io, $commands, $name);
  141. if ($shell instanceof Shell) {
  142. $result = $this->runShell($shell, $argv);
  143. }
  144. if ($shell instanceof Command) {
  145. $result = $shell->run($argv, $io);
  146. }
  147. if ($result === null || $result === true) {
  148. return Command::CODE_SUCCESS;
  149. }
  150. if (is_int($result)) {
  151. return $result;
  152. }
  153. return Command::CODE_ERROR;
  154. }
  155. /**
  156. * Application bootstrap wrapper.
  157. *
  158. * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`.
  159. * After the application is bootstrapped and events are attached, plugins are bootstrapped
  160. * and have their events attached.
  161. *
  162. * @return void
  163. */
  164. protected function bootstrap(): void
  165. {
  166. $this->app->bootstrap();
  167. if ($this->app instanceof PluginApplicationInterface) {
  168. $this->app->pluginBootstrap();
  169. }
  170. }
  171. /**
  172. * Get the application's event manager or the global one.
  173. *
  174. * @return \Cake\Event\EventManagerInterface
  175. */
  176. public function getEventManager(): EventManagerInterface
  177. {
  178. if ($this->app instanceof PluginApplicationInterface) {
  179. return $this->app->getEventManager();
  180. }
  181. return EventManager::instance();
  182. }
  183. /**
  184. * Get/set the application's event manager.
  185. *
  186. * If the application does not support events and this method is used as
  187. * a setter, an exception will be raised.
  188. *
  189. * @param \Cake\Event\EventManagerInterface $events The event manager to set.
  190. * @return $this
  191. */
  192. public function setEventManager(EventManagerInterface $events): EventDispatcherInterface
  193. {
  194. if ($this->app instanceof PluginApplicationInterface) {
  195. $this->app->setEventManager($events);
  196. return $this;
  197. }
  198. throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
  199. }
  200. /**
  201. * Get the shell instance for a given command name
  202. *
  203. * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
  204. * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
  205. * @param string $name The command name to find
  206. * @return \Cake\Console\Shell|\Cake\Console\Command
  207. */
  208. protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
  209. {
  210. $instance = $commands->get($name);
  211. if (is_string($instance)) {
  212. $instance = $this->createShell($instance, $io);
  213. }
  214. if ($instance instanceof Shell) {
  215. $instance->setRootName($this->root);
  216. }
  217. if ($instance instanceof Command) {
  218. $instance->setName("{$this->root} {$name}");
  219. }
  220. if ($instance instanceof CommandCollectionAwareInterface) {
  221. $instance->setCommandCollection($commands);
  222. }
  223. return $instance;
  224. }
  225. /**
  226. * Build the longest command name that exists in the collection
  227. *
  228. * Build the longest command name that matches a
  229. * defined command. This will traverse a maximum of 3 tokens.
  230. *
  231. * @param \Cake\Console\CommandCollection $commands The command collection to check.
  232. * @param array $argv The CLI arguments.
  233. * @return array An array of the resolved name and modified argv.
  234. */
  235. protected function longestCommandName($commands, $argv)
  236. {
  237. for ($i = 3; $i > 1; $i--) {
  238. $parts = array_slice($argv, 0, $i);
  239. $name = implode(' ', $parts);
  240. if ($commands->has($name)) {
  241. return [$name, array_slice($argv, $i)];
  242. }
  243. }
  244. $name = array_shift($argv);
  245. return [$name, $argv];
  246. }
  247. /**
  248. * Resolve the command name into a name that exists in the collection.
  249. *
  250. * Apply backwards compatible inflections and aliases.
  251. * Will step forward up to 3 tokens in $argv to generate
  252. * a command name in the CommandCollection. More specific
  253. * command names take precedence over less specific ones.
  254. *
  255. * @param \Cake\Console\CommandCollection $commands The command collection to check.
  256. * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
  257. * @param string|null $name The name from the CLI args.
  258. * @return string The resolved name.
  259. */
  260. protected function resolveName(CommandCollection $commands, ConsoleIo $io, ?string $name): string
  261. {
  262. if (!$name) {
  263. $io->err('<error>No command provided. Choose one of the available commands.</error>', 2);
  264. $name = 'help';
  265. }
  266. if (isset($this->aliases[$name])) {
  267. $name = $this->aliases[$name];
  268. }
  269. if (!$commands->has($name)) {
  270. $name = Inflector::underscore($name);
  271. }
  272. if (!$commands->has($name)) {
  273. throw new RuntimeException(
  274. "Unknown command `{$this->root} {$name}`." .
  275. " Run `{$this->root} --help` to get the list of valid commands."
  276. );
  277. }
  278. return $name;
  279. }
  280. /**
  281. * Execute a Shell class.
  282. *
  283. * @param \Cake\Console\Shell $shell The shell to run.
  284. * @param array $argv The CLI arguments to invoke.
  285. * @return int|bool|null Exit code
  286. */
  287. protected function runShell(Shell $shell, array $argv)
  288. {
  289. try {
  290. $shell->initialize();
  291. return $shell->runCommand($argv, true);
  292. } catch (StopException $e) {
  293. $code = $e->getCode();
  294. return $code === null ? $code : (int)$code;
  295. }
  296. }
  297. /**
  298. * The wrapper for creating shell instances.
  299. *
  300. * @param string $className Shell class name.
  301. * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
  302. * @return \Cake\Console\Shell|\Cake\Console\Command
  303. */
  304. protected function createShell($className, ConsoleIo $io)
  305. {
  306. $shell = $this->factory->create($className);
  307. if ($shell instanceof Shell) {
  308. $shell->setIo($io);
  309. }
  310. return $shell;
  311. }
  312. /**
  313. * Ensure that the application's routes are loaded.
  314. *
  315. * Console commands and shells often need to generate URLs.
  316. *
  317. * @return void
  318. */
  319. protected function loadRoutes(): void
  320. {
  321. $builder = Router::createRouteBuilder('/');
  322. if ($this->app instanceof HttpApplicationInterface) {
  323. $this->app->routes($builder);
  324. }
  325. if ($this->app instanceof PluginApplicationInterface) {
  326. $this->app->pluginRoutes($builder);
  327. }
  328. }
  329. }