Provide a way to run Drush processes with less boilerplate
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?
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.
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.