CommandRunner.php 10 KB

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