drush icon indicating copy to clipboard operation
drush copied to clipboard

Provide a way to run Drush processes with less boilerplate

Open TravisCarden opened this issue 8 months ago • 2 comments

It takes a fair bit of setup and know-how to run an ad hoc Drush process from code, e.g., within a custom command class. In a recent code review someone remarked on a convenience method I have for it and suggested I contribute it upstream. I agreed to share it and see what you thought. Maybe you'll tell me I'm making it more complicated than it needs to be. 🙂

So suppose I want to run this Drush command in a process:

drush site:install demo_umami --config-dir=example/config/dir --existing-config

I was hoping to do something like this:

<?php

namespace Drupal\example\Drush\Commands;

use Drush\Commands\DrushCommands;

final class ExampleCommands extends DrushCommands {

  public function example(): void {
    $this->drushProcess(
      'site:install',
      ['demo_umami'],
      ['config-dir' => 'example/config/dir', 'existing-config' => NULL]
    );
  }

}

The simplest I was able to make it was this:

<?php

namespace Drupal\example\Drush\Commands;

use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Drush\Commands\DrushCommands;
use Drush\SiteAlias\SiteAliasManagerAwareInterface;

final class ExampleCommands extends DrushCommands implements SiteAliasManagerAwareInterface {

  use SiteAliasManagerAwareTrait;

  public function example(): void {
    $process = $this->processManager()->drush(
      $this->siteAliasManager()->getSelf(),
      'site:install',
      ['demo_umami'],
      ['config-dir' => 'example/config/dir', 'existing-config' => NULL],
    );

    $process->run();
  }

}

Here's how I generalized it and hid some of that complexity for myself:

<?php

namespace Drupal\example\Drush\Commands;

use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Consolidation\SiteProcess\ProcessBase;
use Drush\Commands\DrushCommands;
use Drush\SiteAlias\SiteAliasManagerAwareInterface;

final class ExampleCommands extends DrushCommands implements SiteAliasManagerAwareInterface {

  use SiteAliasManagerAwareTrait;

  public function example(): void {
    $this->drushProcess(
      'site:install',
      ['demo_umami'],
      ['config-dir' => 'example/config/dir', 'existing-config' => NULL]
    );
  }

  /**
   * Runs a Drush command.
   *
   * @param string $command
   *   The Drush command to execute, e.g., "site:install".
   * @param array $args
   *   An array of arguments to pass to the command, e.g., "['demo_umami']".
   * @param array $options
   *   An array of options to pass to the command. For example:
   *   "['config-dir' => 'example/config/dir', 'existing-config' => NULL]".
   *   `--yes` is automatically added to auto-accept the default for all user
   *   prompts.
   *
   * @return \Consolidation\SiteProcess\ProcessBase
   *   The process used by the Drush command.
   */
  private function drushProcess(
    string $command,
    array $args = [],
    array $options = [],
  ): ProcessBase {
    // Auto-accept the default for all user prompts. Allow the behavior to be
    // overridden via `$options`.
    $options = array_merge(['yes' => NULL], $options);

    $process = $this->processManager()->drush(
      $this->siteAliasManager()->getSelf(),
      $command,
      $args,
      $options,
    );

    $process->run();

    return $process;
  }

}

As soon as I need to do the same thing in another class, I'll extract it into an injectable service.

So, did I miss something and make this harder than it needs to be? If so, where is the correct way documented? If not, would you be interested in a contribution in the spirit of the above?

TravisCarden avatar Jul 02 '25 18:07 TravisCarden

Yes, thats how you run Drush in a subprocess.

One small improvement is that we can autowire services from Drush now. Lets use that to get the SiteAliasManager. This way author does have to know about the trait.

<?php

namespace Drupal\example\Drush\Commands;

use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
use Drush\SiteAlias\SiteAliasManagerAwareInterface;

final class ExampleCommands extends DrushCommands {

  use AutowireTrait;

  public function __construct(
    private SiteAliasManagerInterface $siteAliasManager
  )
  {
    parent::__construct();
  }

  public function example(): void {
    $process = $this->processManager()->drush(
      $this->siteAliasManager->getSelf(),
      'site:install',
      ['demo_umami'],
      ['config-dir' => 'example/config/dir', 'existing-config' => NULL],
    );

    $process->mustRun();
  }

}

This same autowire approach will be needed for ProcessManager once #6135 gets in.

As for an injectable service that hardcodes 'self' as a destination, I'm undecided on how much that helps things. I'd inclined to solve this by adding some documentation in the Author section of the site. Ping to @greg-1-anderson for another opinion on the proposed injectable service.

weitzman avatar Jul 02 '25 21:07 weitzman

The fact that I had to provide a site alias at all is what confused me. It seemed like a lot of overhead to tell it @self, especially when I don't have to specify it on the command line. I thought surely I was making it harder than it needed to be. If the $siteAlias argument to \Drush\SiteAlias\ProcessManager::drush() had been nullable or something, I probably wouldn't have given it another thought.

TravisCarden avatar Jul 03 '25 14:07 TravisCarden