app = $app; $this->root = $root; $this->factory = $factory ?: new CommandFactory(); $this->aliases = [ '--version' => 'version', '--help' => 'help', '-h' => 'help', ]; } /** * Replace the entire alias map for a runner. * * Aliases allow you to define alternate names for commands * in the collection. This can be useful to add top level switches * like `--version` or `-h` * * ### Usage * * ``` * $runner->setAliases(['--version' => 'version']); * ``` * * @param array $aliases The map of aliases to replace. * @return $this */ public function setAliases(array $aliases): self { $this->aliases = $aliases; return $this; } /** * Run the command contained in $argv. * * Use the application to do the following: * * - Bootstrap the application * - Create the CommandCollection using the console() hook on the application. * - Trigger the `Console.buildCommands` event of auto-wiring plugins. * - Run the requested command. * * @param array $argv The arguments from the CLI environment. * @param \Cake\Console\ConsoleIo|null $io The ConsoleIo instance. Used primarily for testing. * @return int The exit code of the command. * @throws \RuntimeException */ public function run(array $argv, ?ConsoleIo $io = null): int { $this->bootstrap(); $commands = new CommandCollection([ 'version' => VersionCommand::class, 'help' => HelpCommand::class, ]); $commands = $this->app->console($commands); if ($this->app instanceof PluginApplicationInterface) { $commands = $this->app->pluginConsole($commands); } $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]); $this->loadRoutes(); if (empty($argv)) { throw new RuntimeException("Cannot run any commands. No arguments received."); } // Remove the root executable segment array_shift($argv); $io = $io ?: new ConsoleIo(); [$name, $argv] = $this->longestCommandName($commands, $argv); $name = $this->resolveName($commands, $io, $name); $result = Command::CODE_ERROR; $shell = $this->getShell($io, $commands, $name); if ($shell instanceof Shell) { $result = $this->runShell($shell, $argv); } if ($shell instanceof Command) { $result = $shell->run($argv, $io); } if ($result === null || $result === true) { return Command::CODE_SUCCESS; } if (is_int($result)) { return $result; } return Command::CODE_ERROR; } /** * Application bootstrap wrapper. * * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`. * After the application is bootstrapped and events are attached, plugins are bootstrapped * and have their events attached. * * @return void */ protected function bootstrap(): void { $this->app->bootstrap(); if ($this->app instanceof PluginApplicationInterface) { $this->app->pluginBootstrap(); } } /** * Get the application's event manager or the global one. * * @return \Cake\Event\EventManagerInterface */ public function getEventManager(): EventManagerInterface { if ($this->app instanceof PluginApplicationInterface) { return $this->app->getEventManager(); } return EventManager::instance(); } /** * Get/set the application's event manager. * * If the application does not support events and this method is used as * a setter, an exception will be raised. * * @param \Cake\Event\EventManagerInterface $events The event manager to set. * @return $this */ public function setEventManager(EventManagerInterface $events): EventDispatcherInterface { if ($this->app instanceof PluginApplicationInterface) { $this->app->setEventManager($events); return $this; } throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.'); } /** * Get the shell instance for a given command name * * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class. * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in. * @param string $name The command name to find * @return \Cake\Console\Shell|\Cake\Console\Command */ protected function getShell(ConsoleIo $io, CommandCollection $commands, $name) { $instance = $commands->get($name); if (is_string($instance)) { $instance = $this->createShell($instance, $io); } if ($instance instanceof Shell) { $instance->setRootName($this->root); } if ($instance instanceof Command) { $instance->setName("{$this->root} {$name}"); } if ($instance instanceof CommandCollectionAwareInterface) { $instance->setCommandCollection($commands); } return $instance; } /** * Build the longest command name that exists in the collection * * Build the longest command name that matches a * defined command. This will traverse a maximum of 3 tokens. * * @param \Cake\Console\CommandCollection $commands The command collection to check. * @param array $argv The CLI arguments. * @return array An array of the resolved name and modified argv. */ protected function longestCommandName($commands, $argv) { for ($i = 3; $i > 1; $i--) { $parts = array_slice($argv, 0, $i); $name = implode(' ', $parts); if ($commands->has($name)) { return [$name, array_slice($argv, $i)]; } } $name = array_shift($argv); return [$name, $argv]; } /** * Resolve the command name into a name that exists in the collection. * * Apply backwards compatible inflections and aliases. * Will step forward up to 3 tokens in $argv to generate * a command name in the CommandCollection. More specific * command names take precedence over less specific ones. * * @param \Cake\Console\CommandCollection $commands The command collection to check. * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors. * @param string|null $name The name from the CLI args. * @return string The resolved name. */ protected function resolveName(CommandCollection $commands, ConsoleIo $io, ?string $name): string { if (!$name) { $io->err('No command provided. Choose one of the available commands.', 2); $name = 'help'; } if (isset($this->aliases[$name])) { $name = $this->aliases[$name]; } if (!$commands->has($name)) { $name = Inflector::underscore($name); } if (!$commands->has($name)) { throw new RuntimeException( "Unknown command `{$this->root} {$name}`." . " Run `{$this->root} --help` to get the list of valid commands." ); } return $name; } /** * Execute a Shell class. * * @param \Cake\Console\Shell $shell The shell to run. * @param array $argv The CLI arguments to invoke. * @return int|bool|null Exit code */ protected function runShell(Shell $shell, array $argv) { try { $shell->initialize(); return $shell->runCommand($argv, true); } catch (StopException $e) { $code = $e->getCode(); return $code === null ? $code : (int)$code; } } /** * The wrapper for creating shell instances. * * @param string $className Shell class name. * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class. * @return \Cake\Console\Shell|\Cake\Console\Command */ protected function createShell($className, ConsoleIo $io) { $shell = $this->factory->create($className); if ($shell instanceof Shell) { $shell->setIo($io); } return $shell; } /** * Ensure that the application's routes are loaded. * * Console commands and shells often need to generate URLs. * * @return void */ protected function loadRoutes(): void { $builder = Router::createRouteBuilder('/'); if ($this->app instanceof HttpApplicationInterface) { $this->app->routes($builder); } if ($this->app instanceof PluginApplicationInterface) { $this->app->pluginRoutes($builder); } } }