CommandRunner.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 application console commands are being run for.
  52. *
  53. * @var \Cake\Console\CommandFactoryInterface
  54. */
  55. protected $factory;
  56. /**
  57. * The root command name. Defaults to `cake`.
  58. *
  59. * @var string
  60. */
  61. protected $root;
  62. /**
  63. * Alias mappings.
  64. *
  65. * @var array
  66. */
  67. protected $aliases = [];
  68. /**
  69. * Constructor
  70. *
  71. * @param \Cake\Core\ConsoleApplicationInterface $app The application to run CLI commands for.
  72. * @param string $root The root command name to be removed from argv.
  73. * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance.
  74. */
  75. public function __construct(ConsoleApplicationInterface $app, $root = 'cake', CommandFactoryInterface $factory = null)
  76. {
  77. $this->app = $app;
  78. $this->root = $root;
  79. $this->factory = $factory ?: new CommandFactory();
  80. $this->aliases = [
  81. '--version' => 'version',
  82. '--help' => 'help',
  83. '-h' => 'help',
  84. ];
  85. }
  86. /**
  87. * Replace the entire alias map for a runner.
  88. *
  89. * Aliases allow you to define alternate names for commands
  90. * in the collection. This can be useful to add top level switches
  91. * like `--version` or `-h`
  92. *
  93. * ### Usage
  94. *
  95. * ```
  96. * $runner->setAliases(['--version' => 'version']);
  97. * ```
  98. *
  99. * @param array $aliases The map of aliases to replace.
  100. * @return $this
  101. */
  102. public function setAliases(array $aliases)
  103. {
  104. $this->aliases = $aliases;
  105. return $this;
  106. }
  107. /**
  108. * Run the command contained in $argv.
  109. *
  110. * Use the application to do the following:
  111. *
  112. * - Bootstrap the application
  113. * - Create the CommandCollection using the console() hook on the application.
  114. * - Trigger the `Console.buildCommands` event of auto-wiring plugins.
  115. * - Run the requested command.
  116. *
  117. * @param array $argv The arguments from the CLI environment.
  118. * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing.
  119. * @return int The exit code of the command.
  120. * @throws \RuntimeException
  121. */
  122. public function run(array $argv, ConsoleIo $io = null)
  123. {
  124. $this->bootstrap();
  125. $commands = new CommandCollection([
  126. 'version' => VersionCommand::class,
  127. 'help' => HelpCommand::class,
  128. ]);
  129. $commands = $this->app->console($commands);
  130. $this->checkCollection($commands, 'console');
  131. if ($this->app instanceof PluginApplicationInterface) {
  132. $commands = $this->app->pluginConsole($commands);
  133. }
  134. $this->checkCollection($commands, 'pluginConsole');
  135. $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
  136. if (empty($argv)) {
  137. throw new RuntimeException("Cannot run any commands. No arguments received.");
  138. }
  139. // Remove the root executable segment
  140. array_shift($argv);
  141. $io = $io ?: new ConsoleIo();
  142. $name = $this->resolveName($commands, $io, array_shift($argv));
  143. $result = Shell::CODE_ERROR;
  144. $shell = $this->getShell($io, $commands, $name);
  145. if ($shell instanceof Shell) {
  146. $result = $this->runShell($shell, $argv);
  147. }
  148. if ($shell instanceof Command) {
  149. $result = $shell->run($argv, $io);
  150. }
  151. if ($result === null || $result === true) {
  152. return Shell::CODE_SUCCESS;
  153. }
  154. if (is_int($result)) {
  155. return $result;
  156. }
  157. return Shell::CODE_ERROR;
  158. }
  159. /**
  160. * Application bootstrap wrapper.
  161. *
  162. * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`.
  163. * After the application is bootstrapped and events are attached, plugins are bootstrapped
  164. * and have their events attached.
  165. *
  166. * @return void
  167. */
  168. protected function bootstrap()
  169. {
  170. $this->app->bootstrap();
  171. if ($this->app instanceof PluginApplicationInterface) {
  172. $this->app->pluginBootstrap();
  173. }
  174. }
  175. /**
  176. * Check the created CommandCollection
  177. *
  178. * @param mixed $commands The CommandCollection to check, could be anything though.
  179. * @param string $method The method that was used.
  180. * @return void
  181. * @throws \RuntimeException
  182. * @deprecated 3.6.0 This method should be replaced with return types in 4.x
  183. */
  184. protected function checkCollection($commands, $method)
  185. {
  186. if (!($commands instanceof CommandCollection)) {
  187. $type = getTypeName($commands);
  188. throw new RuntimeException(
  189. "The application's `{$method}` method did not return a CommandCollection." .
  190. " Got '{$type}' instead."
  191. );
  192. }
  193. }
  194. /**
  195. * Get the application's event manager or the global one.
  196. *
  197. * @return \Cake\Event\EventManagerInterface
  198. */
  199. public function getEventManager()
  200. {
  201. if ($this->app instanceof PluginApplicationInterface) {
  202. return $this->app->getEventManager();
  203. }
  204. return EventManager::instance();
  205. }
  206. /**
  207. * Get/set the application's event manager.
  208. *
  209. * If the application does not support events and this method is used as
  210. * a setter, an exception will be raised.
  211. *
  212. * @param \Cake\Event\EventManager|null $events The event manager to set.
  213. * @return \Cake\Event\EventManager|$this
  214. * @deprecated 3.6.0 Will be removed in 4.0
  215. */
  216. public function eventManager(EventManager $events = null)
  217. {
  218. deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.');
  219. if ($events === null) {
  220. return $this->getEventManager();
  221. }
  222. return $this->setEventManager($events);
  223. }
  224. /**
  225. * Get/set the application's event manager.
  226. *
  227. * If the application does not support events and this method is used as
  228. * a setter, an exception will be raised.
  229. *
  230. * @param \Cake\Event\EventManager $events The event manager to set.
  231. * @return $this
  232. */
  233. public function setEventManager(EventManager $events)
  234. {
  235. if ($this->app instanceof PluginApplicationInterface) {
  236. $this->app->setEventManager($events);
  237. return $this;
  238. }
  239. throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
  240. }
  241. /**
  242. * Get the shell instance for a given command name
  243. *
  244. * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
  245. * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
  246. * @param string $name The command name to find
  247. * @return \Cake\Console\Shell|\Cake\Console\Command
  248. */
  249. protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
  250. {
  251. $instance = $commands->get($name);
  252. if (is_string($instance)) {
  253. $instance = $this->createShell($instance, $io);
  254. }
  255. if ($instance instanceof Shell) {
  256. $instance->setRootName($this->root);
  257. }
  258. if ($instance instanceof Command) {
  259. $instance->setName("{$this->root} {$name}");
  260. }
  261. if ($instance instanceof CommandCollectionAwareInterface) {
  262. $instance->setCommandCollection($commands);
  263. }
  264. return $instance;
  265. }
  266. /**
  267. * Resolve the command name into a name that exists in the collection.
  268. *
  269. * Apply backwards compatibile inflections and aliases.
  270. *
  271. * @param \Cake\Console\CommandCollection $commands The command collection to check.
  272. * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
  273. * @param string $name The name from the CLI args.
  274. * @return string The resolved name.
  275. */
  276. protected function resolveName($commands, $io, $name)
  277. {
  278. if (!$name) {
  279. $io->err('<error>No command provided. Choose one of the available commands.</error>', 2);
  280. $name = 'help';
  281. }
  282. if (isset($this->aliases[$name])) {
  283. $name = $this->aliases[$name];
  284. }
  285. if (!$commands->has($name)) {
  286. $name = Inflector::underscore($name);
  287. }
  288. if (!$commands->has($name)) {
  289. throw new RuntimeException(
  290. "Unknown command `{$this->root} {$name}`." .
  291. " Run `{$this->root} --help` to get the list of valid commands."
  292. );
  293. }
  294. return $name;
  295. }
  296. /**
  297. * Execute a Shell class.
  298. *
  299. * @param \Cake\Console\Shell $shell The shell to run.
  300. * @param array $argv The CLI arguments to invoke.
  301. * @return int Exit code
  302. */
  303. protected function runShell(Shell $shell, array $argv)
  304. {
  305. try {
  306. $shell->initialize();
  307. return $shell->runCommand($argv, true);
  308. } catch (StopException $e) {
  309. return $e->getCode();
  310. }
  311. }
  312. /**
  313. * The wrapper for creating shell instances.
  314. *
  315. * @param string $className Shell class name.
  316. * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
  317. * @return \Cake\Console\Shell|\Cake\Console\Command
  318. */
  319. protected function createShell($className, ConsoleIo $io)
  320. {
  321. $shell = $this->factory->create($className);
  322. if ($shell instanceof Shell) {
  323. $shell->setIo($io);
  324. }
  325. return $shell;
  326. }
  327. }