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:
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\Routing\Router;
27: use Cake\Utility\Inflector;
28: use InvalidArgumentException;
29: use RuntimeException;
30:
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: /**
46: * The application console commands are being run for.
47: *
48: * @var \Cake\Core\ConsoleApplicationInterface
49: */
50: protected $app;
51:
52: /**
53: * The application console commands are being run for.
54: *
55: * @var \Cake\Console\CommandFactoryInterface
56: */
57: protected $factory;
58:
59: /**
60: * The root command name. Defaults to `cake`.
61: *
62: * @var string
63: */
64: protected $root;
65:
66: /**
67: * Alias mappings.
68: *
69: * @var array
70: */
71: protected $aliases = [];
72:
73: /**
74: * Constructor
75: *
76: * @param \Cake\Core\ConsoleApplicationInterface $app The application to run CLI commands for.
77: * @param string $root The root command name to be removed from argv.
78: * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance.
79: */
80: public function __construct(ConsoleApplicationInterface $app, $root = 'cake', CommandFactoryInterface $factory = null)
81: {
82: $this->app = $app;
83: $this->root = $root;
84: $this->factory = $factory ?: new CommandFactory();
85: $this->aliases = [
86: '--version' => 'version',
87: '--help' => 'help',
88: '-h' => 'help',
89: ];
90: }
91:
92: /**
93: * Replace the entire alias map for a runner.
94: *
95: * Aliases allow you to define alternate names for commands
96: * in the collection. This can be useful to add top level switches
97: * like `--version` or `-h`
98: *
99: * ### Usage
100: *
101: * ```
102: * $runner->setAliases(['--version' => 'version']);
103: * ```
104: *
105: * @param array $aliases The map of aliases to replace.
106: * @return $this
107: */
108: public function setAliases(array $aliases)
109: {
110: $this->aliases = $aliases;
111:
112: return $this;
113: }
114:
115: /**
116: * Run the command contained in $argv.
117: *
118: * Use the application to do the following:
119: *
120: * - Bootstrap the application
121: * - Create the CommandCollection using the console() hook on the application.
122: * - Trigger the `Console.buildCommands` event of auto-wiring plugins.
123: * - Run the requested command.
124: *
125: * @param array $argv The arguments from the CLI environment.
126: * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing.
127: * @return int The exit code of the command.
128: * @throws \RuntimeException
129: */
130: public function run(array $argv, ConsoleIo $io = null)
131: {
132: $this->bootstrap();
133:
134: $commands = new CommandCollection([
135: 'version' => VersionCommand::class,
136: 'help' => HelpCommand::class,
137: ]);
138: $commands = $this->app->console($commands);
139: $this->checkCollection($commands, 'console');
140:
141: if ($this->app instanceof PluginApplicationInterface) {
142: $commands = $this->app->pluginConsole($commands);
143: }
144: $this->checkCollection($commands, 'pluginConsole');
145: $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
146: $this->loadRoutes();
147:
148: if (empty($argv)) {
149: throw new RuntimeException("Cannot run any commands. No arguments received.");
150: }
151: // Remove the root executable segment
152: array_shift($argv);
153:
154: $io = $io ?: new ConsoleIo();
155:
156: list($name, $argv) = $this->longestCommandName($commands, $argv);
157: $name = $this->resolveName($commands, $io, $name);
158:
159: $result = Command::CODE_ERROR;
160: $shell = $this->getShell($io, $commands, $name);
161: if ($shell instanceof Shell) {
162: $result = $this->runShell($shell, $argv);
163: }
164: if ($shell instanceof Command) {
165: $result = $this->runCommand($shell, $argv, $io);
166: }
167:
168: if ($result === null || $result === true) {
169: return Command::CODE_SUCCESS;
170: }
171: if (is_int($result)) {
172: return $result;
173: }
174:
175: return Command::CODE_ERROR;
176: }
177:
178: /**
179: * Application bootstrap wrapper.
180: *
181: * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`.
182: * After the application is bootstrapped and events are attached, plugins are bootstrapped
183: * and have their events attached.
184: *
185: * @return void
186: */
187: protected function bootstrap()
188: {
189: $this->app->bootstrap();
190: if ($this->app instanceof PluginApplicationInterface) {
191: $this->app->pluginBootstrap();
192: }
193: }
194:
195: /**
196: * Check the created CommandCollection
197: *
198: * @param mixed $commands The CommandCollection to check, could be anything though.
199: * @param string $method The method that was used.
200: * @return void
201: * @throws \RuntimeException
202: * @deprecated 3.6.0 This method should be replaced with return types in 4.x
203: */
204: protected function checkCollection($commands, $method)
205: {
206: if (!($commands instanceof CommandCollection)) {
207: $type = getTypeName($commands);
208: throw new RuntimeException(
209: "The application's `{$method}` method did not return a CommandCollection." .
210: " Got '{$type}' instead."
211: );
212: }
213: }
214:
215: /**
216: * Get the application's event manager or the global one.
217: *
218: * @return \Cake\Event\EventManagerInterface
219: */
220: public function getEventManager()
221: {
222: if ($this->app instanceof PluginApplicationInterface) {
223: return $this->app->getEventManager();
224: }
225:
226: return EventManager::instance();
227: }
228:
229: /**
230: * Get/set the application's event manager.
231: *
232: * If the application does not support events and this method is used as
233: * a setter, an exception will be raised.
234: *
235: * @param \Cake\Event\EventManager|null $events The event manager to set.
236: * @return \Cake\Event\EventManager|$this
237: * @deprecated 3.6.0 Will be removed in 4.0
238: */
239: public function eventManager(EventManager $events = null)
240: {
241: deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.');
242: if ($events === null) {
243: return $this->getEventManager();
244: }
245:
246: return $this->setEventManager($events);
247: }
248:
249: /**
250: * Get/set the application's event manager.
251: *
252: * If the application does not support events and this method is used as
253: * a setter, an exception will be raised.
254: *
255: * @param \Cake\Event\EventManager $events The event manager to set.
256: * @return $this
257: */
258: public function setEventManager(EventManager $events)
259: {
260: if ($this->app instanceof PluginApplicationInterface) {
261: $this->app->setEventManager($events);
262:
263: return $this;
264: }
265:
266: throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
267: }
268:
269: /**
270: * Get the shell instance for a given command name
271: *
272: * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
273: * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
274: * @param string $name The command name to find
275: * @return \Cake\Console\Shell|\Cake\Console\Command
276: */
277: protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
278: {
279: $instance = $commands->get($name);
280: if (is_string($instance)) {
281: $instance = $this->createShell($instance, $io);
282: }
283: if ($instance instanceof Shell) {
284: $instance->setRootName($this->root);
285: }
286: if ($instance instanceof Command) {
287: $instance->setName("{$this->root} {$name}");
288: }
289: if ($instance instanceof CommandCollectionAwareInterface) {
290: $instance->setCommandCollection($commands);
291: }
292:
293: return $instance;
294: }
295:
296: /**
297: * Build the longest command name that exists in the collection
298: *
299: * Build the longest command name that matches a
300: * defined command. This will traverse a maximum of 3 tokens.
301: *
302: * @param \Cake\Console\CommandCollection $commands The command collection to check.
303: * @param array $argv The CLI arguments.
304: * @return array An array of the resolved name and modified argv.
305: */
306: protected function longestCommandName($commands, $argv)
307: {
308: for ($i = 3; $i > 1; $i--) {
309: $parts = array_slice($argv, 0, $i);
310: $name = implode(' ', $parts);
311: if ($commands->has($name)) {
312: return [$name, array_slice($argv, $i)];
313: }
314: }
315: $name = array_shift($argv);
316:
317: return [$name, $argv];
318: }
319:
320: /**
321: * Resolve the command name into a name that exists in the collection.
322: *
323: * Apply backwards compatible inflections and aliases.
324: * Will step forward up to 3 tokens in $argv to generate
325: * a command name in the CommandCollection. More specific
326: * command names take precedence over less specific ones.
327: *
328: * @param \Cake\Console\CommandCollection $commands The command collection to check.
329: * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
330: * @param string $name The name
331: * @return string The resolved class name
332: */
333: protected function resolveName($commands, $io, $name)
334: {
335: if (!$name) {
336: $io->err('<error>No command provided. Choose one of the available commands.</error>', 2);
337: $name = 'help';
338: }
339: if (isset($this->aliases[$name])) {
340: $name = $this->aliases[$name];
341: }
342: if (!$commands->has($name)) {
343: $name = Inflector::underscore($name);
344: }
345: if (!$commands->has($name)) {
346: throw new RuntimeException(
347: "Unknown command `{$this->root} {$name}`." .
348: " Run `{$this->root} --help` to get the list of valid commands."
349: );
350: }
351:
352: return $name;
353: }
354:
355: /**
356: * Execute a Command class.
357: *
358: * @param \Cake\Console\Command $command The command to run.
359: * @param array $argv The CLI arguments to invoke.
360: * @param \Cake\Console\ConsoleIo $io The console io
361: * @return int Exit code
362: */
363: protected function runCommand(Command $command, array $argv, ConsoleIo $io)
364: {
365: try {
366: return $command->run($argv, $io);
367: } catch (StopException $e) {
368: return $e->getCode();
369: }
370: }
371:
372: /**
373: * Execute a Shell class.
374: *
375: * @param \Cake\Console\Shell $shell The shell to run.
376: * @param array $argv The CLI arguments to invoke.
377: * @return int Exit code
378: */
379: protected function runShell(Shell $shell, array $argv)
380: {
381: try {
382: $shell->initialize();
383:
384: return $shell->runCommand($argv, true);
385: } catch (StopException $e) {
386: return $e->getCode();
387: }
388: }
389:
390: /**
391: * The wrapper for creating shell instances.
392: *
393: * @param string $className Shell class name.
394: * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
395: * @return \Cake\Console\Shell|\Cake\Console\Command
396: */
397: protected function createShell($className, ConsoleIo $io)
398: {
399: $shell = $this->factory->create($className);
400: if ($shell instanceof Shell) {
401: $shell->setIo($io);
402: }
403:
404: return $shell;
405: }
406:
407: /**
408: * Ensure that the application's routes are loaded.
409: *
410: * Console commands and shells often need to generate URLs.
411: *
412: * @return void
413: */
414: protected function loadRoutes()
415: {
416: $builder = Router::createRouteBuilder('/');
417:
418: if ($this->app instanceof HttpApplicationInterface) {
419: $this->app->routes($builder);
420: }
421: if ($this->app instanceof PluginApplicationInterface) {
422: $this->app->pluginRoutes($builder);
423: }
424: }
425: }
426: