4 use Consolidation\AnnotatedCommand\AnnotatedCommand;
5 use Consolidation\AnnotatedCommand\CommandFileDiscovery;
6 use Drush\Boot\BootstrapManager;
7 use Drush\Runtime\TildeExpansionHook;
8 use Consolidation\SiteAlias\SiteAliasManager;
9 use Drush\Log\LogLevel;
10 use Drush\Command\RemoteCommandProxy;
11 use Drush\Runtime\RedispatchHook;
12 use Robo\Common\ConfigAwareTrait;
13 use Robo\Contract\ConfigAwareInterface;
14 use Symfony\Component\Console\Application as SymfonyApplication;
15 use Symfony\Component\Console\Input\InputOption;
16 use Symfony\Component\Console\Exception\CommandNotFoundException;
17 use Symfony\Component\Console\Input\InputInterface;
18 use Symfony\Component\Console\Output\OutputInterface;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
23 * Our application object
25 * Note: Implementing *AwareInterface here does NOT automatically cause
26 * that corresponding service to be injected into the Application. This
27 * is because the application object is created prior to the DI container.
28 * See DependencyInjection::injectApplicationServices() to add more services.
30 class Application extends SymfonyApplication implements LoggerAwareInterface, ConfigAwareInterface
35 /** @var BootstrapManager */
36 protected $bootstrapManager;
38 /** @var SiteAliasManager */
39 protected $aliasManager;
41 /** @var RedispatchHook */
42 protected $redispatchHook;
44 /** @var TildeExpansionHook */
45 protected $tildeExpansionHook;
48 * Add global options to the Application and their default values to Config.
50 public function configureGlobalOptions()
52 $this->getDefinition()
54 new InputOption('--debug', 'd', InputOption::VALUE_NONE, 'Equivalent to -vv')
57 $this->getDefinition()
59 new InputOption('--yes', 'y', InputOption::VALUE_NONE, 'Equivalent to --no-interaction.')
62 // Note that -n belongs to Symfony Console's --no-interaction.
63 $this->getDefinition()
65 new InputOption('--no', null, InputOption::VALUE_NONE, 'Cancels at any confirmation prompt.')
68 $this->getDefinition()
70 new InputOption('--remote-host', null, InputOption::VALUE_REQUIRED, 'Run on a remote server.')
73 $this->getDefinition()
75 new InputOption('--remote-user', null, InputOption::VALUE_REQUIRED, 'The user to use in remote execution.')
78 $this->getDefinition()
80 new InputOption('--root', '-r', InputOption::VALUE_REQUIRED, 'The Drupal root for this site.')
84 $this->getDefinition()
86 new InputOption('--uri', '-l', InputOption::VALUE_REQUIRED, 'Which multisite from the selected root to use.')
89 $this->getDefinition()
91 new InputOption('--simulate', null, InputOption::VALUE_NONE, 'Run in simulated mode (show what would have happened).')
94 // TODO: Implement handling for 'pipe'
95 $this->getDefinition()
97 new InputOption('--pipe', null, InputOption::VALUE_NONE, 'Select the canonical script-friendly output format.')
100 $this->getDefinition()
102 new InputOption('--define', '-D', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Define a configuration item value.', [])
106 public function bootstrapManager()
108 return $this->bootstrapManager;
111 public function setBootstrapManager(BootstrapManager $bootstrapManager)
113 $this->bootstrapManager = $bootstrapManager;
116 public function aliasManager()
118 return $this->aliasManager;
121 public function setAliasManager($aliasManager)
123 $this->aliasManager = $aliasManager;
126 public function setRedispatchHook(RedispatchHook $redispatchHook)
128 $this->redispatchHook = $redispatchHook;
131 public function setTildeExpansionHook(TildeExpansionHook $tildeExpansionHook)
133 $this->tildeExpansionHook = $tildeExpansionHook;
137 * Return the framework uri selected by the user.
139 public function getUri()
141 if (!$this->bootstrapManager) {
144 return $this->bootstrapManager->getUri();
148 * If the user did not explicitly select a site URI,
149 * then pick an appropriate site from the cwd.
151 public function refineUriSelection($cwd)
153 if (!$this->bootstrapManager || !$this->aliasManager) {
156 $selfAliasRecord = $this->aliasManager->getSelf();
157 if (!$selfAliasRecord->hasRoot() && !$this->bootstrapManager()->drupalFinder()->getDrupalRoot()) {
160 $uri = $selfAliasRecord->uri();
163 $uri = $this->selectUri($cwd);
164 $selfAliasRecord->setUri($uri);
165 $this->aliasManager->setSelf($selfAliasRecord);
167 // Update the uri in the bootstrap manager
168 $this->bootstrapManager->setUri($uri);
172 * Select a URI to use for the site, based on directory or config.
174 public function selectUri($cwd)
176 $uri = $this->config->get('options.uri');
180 return $this->bootstrapManager()->selectUri($cwd);
186 public function find($name)
191 $command = $this->bootstrapAndFind($name);
192 // Avoid exception when help is being built by https://github.com/bamarni/symfony-console-autocomplete.
193 // @todo Find a cleaner solution.
194 if (Drush::config()->get('runtime.argv')[1] !== 'help') {
195 $this->checkObsolete($command);
201 * Look up a command. Bootstrap further if necessary.
203 protected function bootstrapAndFind($name)
206 return parent::find($name);
207 } catch (CommandNotFoundException $e) {
208 // Is the unknown command destined for a remote site?
209 if ($this->aliasManager) {
210 $selfAlias = $this->aliasManager->getSelf();
211 if ($selfAlias->isRemote()) {
212 $command = new RemoteCommandProxy($name, $this->redispatchHook);
213 $command->setApplication($this);
217 // If we have no bootstrap manager, then just re-throw
219 if (!$this->bootstrapManager) {
223 $this->logger->log(LogLevel::DEBUG, 'Bootstrap further to find {command}', ['command' => $name]);
224 $this->bootstrapManager->bootstrapMax();
225 $this->logger->log(LogLevel::DEBUG, 'Done with bootstrap max in Application::find(): trying to find {command} again.', ['command' => $name]);
227 if (!$this->bootstrapManager()->hasBootstrapped(DRUSH_BOOTSTRAP_DRUPAL_ROOT)) {
228 // Unable to progress in the bootstrap. Give friendly error message.
229 throw new CommandNotFoundException(dt('Command !command was not found. Pass --root or a @siteAlias in order to run Drupal-specific commands.', ['!command' => $name]));
232 // Try to find it again, now that we bootstrapped as far as possible.
234 return parent::find($name);
235 } catch (CommandNotFoundException $e) {
236 if (!$this->bootstrapManager()->hasBootstrapped(DRUSH_BOOTSTRAP_DRUPAL_DATABASE)) {
237 // Unable to bootstrap to DB. Give targetted error message.
238 throw new CommandNotFoundException(dt('Command !command was not found. Drush was unable to query the database. As a result, many commands are unavailable. Re-run your command with --debug to see relevant log messages.', ['!command' => $name]));
240 if (!$this->bootstrapManager()->hasBootstrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) {
241 // Unable to fully bootstrap. Give targetted error message.
242 throw new CommandNotFoundException(dt('Command !command was not found. Drush successfully connected to the database but was unable to fully bootstrap your site. As a result, many commands are unavailable. Re-run your command with --debug to see relevant log messages.', ['!command' => $name]));
244 // We fully bootstrapped but still could not find command. Rethrow.
252 * If a command is annotated @obsolete, then we will throw an exception
253 * immediately; the command will not run, and no hooks will be called.
255 protected function checkObsolete($command)
257 if (!$command instanceof AnnotatedCommand) {
261 $annotationData = $command->getAnnotationData();
262 if (!$annotationData->has('obsolete')) {
266 $obsoleteMessage = $command->getDescription();
267 throw new \Exception($obsoleteMessage);
273 * Note: This method is called twice, as we wish to configure the IO
274 * objects earlier than Symfony does. We could define a boolean class
275 * field to record when this method is called, and do nothing on the
276 * second call. At the moment, the work done here is trivial, so we let
279 protected function configureIO(InputInterface $input, OutputInterface $output)
281 // Do default Symfony confguration.
282 parent::configureIO($input, $output);
284 // Process legacy Drush global options.
285 // Note that `getParameterOption` returns the VALUE of the option if
286 // it is found, or NULL if it finds an option with no value.
287 if ($input->getParameterOption(['--yes', '-y', '--no', '-n'], false, true) !== false) {
288 $input->setInteractive(false);
290 // Symfony will set these later, but we want it set upfront
291 if ($input->getParameterOption(['--verbose', '-v'], false, true) !== false) {
292 $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
294 // We are not using "very verbose", but set this for completeness
295 if ($input->getParameterOption(['-vv'], false, true) !== false) {
296 $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
298 // Use -vvv of --debug for even more verbose logging.
299 if ($input->getParameterOption(['--debug', '-d', '-vvv'], false, true) !== false) {
300 $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
305 * Configure the application object and register all of the commandfiles
306 * available in the search paths provided via Preflight
308 public function configureAndRegisterCommands(InputInterface $input, OutputInterface $output, $commandfileSearchpath)
310 // Symfony will call this method for us in run() (it will be
311 // called again), but we want to call it up-front, here, so that
312 // our $input and $output objects have been appropriately
313 // configured in case we wish to use them (e.g. for logging) in
314 // any of the configuration steps we do here.
315 $this->configureIO($input, $output);
317 $discovery = $this->commandDiscovery();
318 $commandClasses = $discovery->discover($commandfileSearchpath, '\Drush');
320 $this->loadCommandClasses($commandClasses);
322 // Uncomment the lines below to use Console's built in help and list commands.
323 // unset($commandClasses[__DIR__ . '/Commands/help/HelpCommands.php']);
324 // unset($commandClasses[__DIR__ . '/Commands/help/ListCommands.php']);
326 // Use the robo runner to register commands with Symfony application.
327 // This method could / should be refactored in Robo so that we can use
328 // it without creating a Runner object that we would not otherwise need.
329 $runner = new \Robo\Runner();
330 $runner->registerCommandClasses($this, $commandClasses);
334 * Ensure that any discovered class that is not part of the autoloader
335 * is, in fact, included.
337 protected function loadCommandClasses($commandClasses)
339 foreach ($commandClasses as $file => $commandClass) {
340 if (!class_exists($commandClass)) {
347 * Create a command file discovery object
349 protected function commandDiscovery()
351 $discovery = new CommandFileDiscovery();
353 ->setIncludeFilesAtBase(true)
355 ->ignoreNamespacePart('contrib', 'Commands')
356 ->ignoreNamespacePart('custom', 'Commands')
357 ->ignoreNamespacePart('src')
358 ->setSearchLocations(['Commands', 'Hooks', 'Generators'])
359 ->setSearchPattern('#.*(Command|Hook|Generator)s?.php$#');