From 9cc6cd44a2087b6e7bdf9797f2ac6138a46fec13 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Sat, 9 Mar 2024 16:52:46 -0500 Subject: [PATCH] Allow autowire in commandfiles (#5893) --- docs/bootstrap.md | 3 +- docs/commands.md | 4 +- docs/dependency-injection.md | 102 ++++-------------- docs/generators.md | 2 +- docs/hooks.md | 2 +- docs/site-alias-manager.md | 8 +- src/Attributes/Bootstrap.php | 2 +- src/Commands/AutowireTrait.php | 48 +++++++++ src/Commands/core/LoginCommands.php | 34 +++--- src/Commands/core/MaintCommands.php | 14 +-- src/Commands/sql/SqlCommands.php | 6 +- src/Runtime/DependencyInjection.php | 57 +++++----- src/Runtime/ServiceManager.php | 49 ++++++++- .../src/Drush/Generators/ExampleGenerator.php | 17 +-- 14 files changed, 178 insertions(+), 170 deletions(-) create mode 100644 src/Commands/AutowireTrait.php diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 4cdaff1d3f..ad31d0fa9e 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -43,7 +43,7 @@ Bootstrapping is done from a Symfony Console command hook. The different bootstr none ----------------------- -Only run Drush _preflight_, without considering Drupal at all. Any code that operates on the Drush installation, and not specifically any Drupal directory, should bootstrap to this phase. +Only run Drush _preflight_, without considering Drupal at all. Any code that operates on the Drush installation, and not specifically any Drupal directory, should bootstrap to this phase. This Attribute and value may also be used on a command _class_ when it wants to load before Drupal bootstrap is started. Commands that ship inside Drupal modules always bootstrap to full, regardless of _none_ value. root ------------------------------ @@ -68,4 +68,3 @@ Fully initialize Drupal. This is analogous to the DRUPAL\_BOOTSTRAP\_FULL bootst max --------------------- This is not an actual bootstrap phase. Commands that use the "max" bootstrap level will cause Drush to bootstrap as far as possible, and then run the command regardless of the bootstrap phase that was reached. This is useful for Drush commands that work without a bootstrapped site, but that provide additional information or capabilities in the presence of a bootstrapped site. For example, [`drush status`](commands/core_status.md) will show progressively more information the farther the site bootstraps. - diff --git a/docs/commands.md b/docs/commands.md index cc824a5b62..268719f8c2 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -2,7 +2,7 @@ !!! tip - 1. Drush 12 expects commandfiles to use a [create() method](dependency-injection.md#create-method) to inject Drupal and Drush dependencies. Prior versions used a [drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files) which is now deprecated and will be removed in Drush 13. + 1. You may now use [Autowire](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php) to inject Drupal and Drush dependencies. Prior approaches were using a [create() method](dependency-injection.md#create-method and using a [drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). These are now deprecated. 1. Drush 12 expects all commandfiles in the `/src/Drush/` directory. The `Drush` subdirectory is a new requirement. Creating a new Drush command is easy. Follow the steps below. @@ -11,7 +11,7 @@ Creating a new Drush command is easy. Follow the steps below. 2. Drush will prompt for the machine name of the module that should "own" the file. The module selected must already exist and be enabled. Use `drush generate module` to create a new module. 3. Drush will then report that it created a commandfile. Edit as needed. 4. Use the classes for the core Drush commands at [/src/Commands](https://github.com/drush-ops/drush/tree/12.x/src/Commands) as inspiration and documentation. -5. See the [dependency injection docs](dependency-injection.md) for interfaces you can implement to gain access to Drush config, Drupal site aliases, etc. Also note the [create() method](dependency-injection.md#create-method) for injecting Drupal or Drush dependencies. +5. You may [inject dependencies](dependency-injection.md) into a command instance. 6. Write PHPUnit tests based on [Drush Test Traits](https://github.com/drush-ops/drush/blob/12.x/docs/contribute/unish.md#drush-test-traits). ## Attributes or Annotations diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index 1d8346c2c4..49abead7ea 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -3,37 +3,22 @@ Dependency Injection Drush command files obtain references to the resources they need through a technique called _dependency injection_. When using this programing paradigm, a class by convention will never use the `new` operator to instantiate dependencies. Instead, it will store the other objects it needs in class variables, and provide a way for other code to assign an object to that variable. -Types of Injection ------------------------ +!!! tip -There are two ways that a class can receive its dependencies. One is called “constructor injection”, and the other is called “setter injection”. + Drush 11 and prior required [dependency injection via a drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). This approach is deprecated in Drush 12+. -*Example of constructor injection:* -```php - public function __construct(DependencyType $service) - { - $this->service = $service; - } -``` +Autowire +------------------ +:octicons-tag-24: 12.5+ +Command files may inject Drush and Drupal services by adding the [AutowireTrait](https://github.com/drush-ops/drush/blob/12.x/src/Commands/AutowireTrait.php) to the class (example: [PmCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/pm/MaintCommands.php)). This enables your [Constructor parameter type hints determine the the injected service](https://www.drupal.org/node/3396179). When a type hint is insufficient, an [#[Autowire] Attribute](https://www.drupal.org/node/3396179) on the constructor property (with _service:_ named argument) directs AutoWireTrait to the right service (example: [LoginCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/LoginCommands.php)). This Attribute is currently _required_ when injecting Drush services (not required for Drupal services). -*Example of setter injection:* -```php - public function setService(DependencyType $service) - { - $this->service = $service; - } -``` -The code that is responsible for providing the dependencies a class needs is usually an object called the dependency injection container. +If your command is not found by Drush, add the `-vvv` option for debug info about any service instantiation errors. If Autowire is still insufficient, a commandfile may implement its own `create()` method (see below). create() method ------------------ :octicons-tag-24: 11.6+ -!!! tip - - Drush 11 and prior required [dependency injection via a drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). This approach is deprecated in Drush 12 and will be removed in Drush 13. - -Drush command files can inject services by adding a create() method to the commandfile. See [creating commands](commands.md) for instructions on how to use the Drupal Code Generator to create a simple command file starter. A create() method and a constructor will look something like this: +Command files not using Autowire may inject services by adding a create() method to the commandfile. A create() method and a constructor will look something like this: ```php class WootStaticFactoryCommands extends DrushCommands { @@ -44,76 +29,27 @@ class WootStaticFactoryCommands extends DrushCommands $this->configFactory = $configFactory; } - public static function create(ContainerInterface $container, DrushContainer $drush): self + public static function create(ContainerInterface $container): self { return new static($container->get('config.factory')); } ``` -See the [Drupal Documentation](https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/services-and-dependency-injection-in-drupal-8#s-injecting-dependencies-into-controllers-forms-and-blocks) for details on how to inject Drupal services into your command file. Drush's approach mimics Drupal's blocks, forms, and controllers. - -Note that if you do not need to pull any services from the Drush container, then you may -omit the second parameter to the `create()` method. +See the [Drupal Documentation](https://www.drupal.org/docs/drupal-apis/services-and-dependency-injection/services-and-dependency-injection-in-drupal-8#s-injecting-dependencies-into-controllers-forms-and-blocks) for details on how to inject Drupal services into your command file. This approach mimics Drupal's blocks, forms, and controllers. createEarly() method ------------------ :octicons-tag-24: 12.0+ -Drush commands that need to be instantiated prior to bootstrap may do so by -utilizing the `createEarly()` static factory. This method looks and functions -exacty like the `create()` static factory, except it is only passed the Drush -container. The Drupal container is not available to command handlers that use -`createEarly()`. - -Note also that Drush commands packaged with Drupal modules are not discovered -until after Drupal bootstraps, and therefore cannot use `createEarly()`. This -mechanism is only usable by PSR-4 discovered commands packaged with Composer -projects that are *not* Drupal modules. - -Inflection -------------- - !!! tip - Inflection is deprecated in Drush 12; use `create()` or `createEarly()` instead. - Some classes are no longer available for inflection in Drush 12, and more (or potentially all) - may be removed in Drush 13. - -Drush will also inject dependencies that it provides using a technique called inflection. Inflection is a kind of dependency injection that works by way of a set of provided inflection interfaces, one for each available service. Each of these interfaces will define one or more setter methods (usually only one); these will automatically be called by Drush when the commandfile object is instantiated. The command only needs to implement this method and save the provided object in a class field. There is usually a corresponding trait that may be included via a `use` statement to fulfill this requirement. - -For example: - -```php -siteAliasManager()->getSelf(); - $this->logger()->success(‘The current alias is {name}’, [‘name’ => $selfAlias]); - return new ListDataFromKeys($aliasRecord->export()); - } -} -``` + The createEarly() method was deprecated in Drush 12.5. Instead put a `#[CLI\Bootstrap(DrupalBootLevels::NONE)]` Attribute on the command class and inject dependencies via the usual `__construct` with [AutowireTrait](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php). Note also that Drush commands packaged with Drupal modules are not discovered + until after Drupal bootstraps, and therefore cannot use `createEarly()`. This + mechanism is only usable by PSR-4 discovered commands packaged with Composer + projects that are *not* Drupal modules. -All Drush command files extend DrushCommands. DrushCommands implements ConfigAwareInterface, IOAwareInterface, LoggerAwareInterface, which gives access to `$this->getConfig()`, `$this->logger()` and other ways to do input and output. See the [IO documentation](io.md) for more information. -Any additional services that are desired must be injected by implementing the appropriate inflection interface. - -Additional Interfaces: - -- SiteAliasManagerAwareInterface: The site alias manager [allows alias records to be obtained](site-alias-manager.md). -- CustomEventAwareInterface: Allows command files to [define and fire custom events](hooks.md) that other command files can hook. +Inflection +----------------- +A command class may implement the following interfaces. When doing so, implement the corresponding trait to satisfy the interface. -Note that although the autoloader and Drush dependency injection container is available and may be injected into your command file if needed, this should be avoided. Favor using services that can be injected from Drupal or Drush. Some of the objects in the container are not part of the Drush public API, and may not maintain compatibility in minor and patch releases. +- [CustomEventAwareInterface](https://github.com/consolidation/annotated-command/blob/4.x/src/Events/CustomEventAwareInterface.php): Allows command files to [define and fire custom events](hooks.md) that other command files can hook. Example: [CacheCommands](https://github.com/drush-ops/drush/blob/13.x/src/Commands/core/CacheCommands.php) +- [StdinAwareInterface](https://github.com/consolidation/annotated-command/blob/4.x/src/Input/StdinAwareInterface.php): Read from standard input. This class contains facilities to redirect stdin to instead read from a file, e.g. in response to an option or argument value. Example: [CacheCommands](https://github.com/drush-ops/drush/blob/13.x/src/Commands/core/CacheCommands.php) diff --git a/docs/generators.md b/docs/generators.md index 6962efd945..4aa329b3fa 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -2,7 +2,7 @@ !!! tip - Drush 11 and prior required generators to define a [drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). This is no longer used with Drush 12+ generators. See [create() method](dependency-injection.md#create-method) for injecting dependencies. + Drush 11 and prior required generators to define a [drush.services.yml file](https://www.drush.org/11.x/dependency-injection/#services-files). This is no longer used with Drush 12+ generators. See [docs](dependency-injection.md) for injecting dependencies.. Generators jump start your coding by building all the boring boilerplate code for you. After running the [generate command](commands/generate.md), you have a guide for where to insert your custom logic. diff --git a/docs/hooks.md b/docs/hooks.md index 2695b8569c..c503549b73 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -9,7 +9,7 @@ All commandfiles may implement methods that are called by Drush at various times Drush commands can define custom events that other command files can hook. You can find examples in [CacheCommands](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/CacheCommands.php) and [SanitizeCommands](https://github.com/drush-ops/drush/blob/12.x/src/Drupal/Commands/sql/SanitizeCommands.php) -First, the command must implement CustomEventAwareInterface and use CustomEventAwareTrait, as described in the [dependency injection](dependency-injection.md) documentation. +First, the command must implement CustomEventAwareInterface and use CustomEventAwareTrait, as described in the [dependency injection](dependency-injection.md#inflection) documentation. Then, the command may ask the provided hook manager to return a list of handlers with a certain attribute. In the example below, the `my-event` label is used: ```php diff --git a/docs/site-alias-manager.md b/docs/site-alias-manager.md index dea904955f..2f25a43a78 100644 --- a/docs/site-alias-manager.md +++ b/docs/site-alias-manager.md @@ -4,8 +4,12 @@ Site Alias Manager The [Site Alias Manager (SAM)](https://github.com/consolidation/site-alias/blob/4.0.1/src/SiteAliasManager.php) service is used to retrieve information about one or all of the site aliases for the current installation. - An informative example is the [browse command](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/BrowseCommands.php) -- A commandfile gets access to the SAM by implementing the SiteAliasManagerAwareInterface and *use*ing the SiteAliasManagerAwareTrait trait. Then you gain access via `$this->siteAliasManager()`. -- If an alias was used for the current request, it is available via `$this->siteAliasManager()->getself()`. +- A commandfile gets access to the SAM as follows: +```php +#[Autowire(service: DependencyInjection::SITE_ALIAS_MANAGER)] +private readonly SiteAliasManagerInterface $siteAliasManager +``` +- If an alias was used for the current request, it is available via `$this->siteAliasManager->getself()`. - The SAM generally deals in [SiteAlias](https://github.com/consolidation/site-alias/blob/main/src/SiteAlias.php) objects. That is how any given site alias is represented. See its methods for determining things like whether the alias points to a local host or remote host. - [Site alias docs](site-aliases.md). - [Dynamically alter site aliases](https://raw.githubusercontent.com/drush-ops/drush/11.x/examples/Commands/SiteAliasAlterCommands.php). diff --git a/src/Attributes/Bootstrap.php b/src/Attributes/Bootstrap.php index 43e5543705..7f558c9253 100644 --- a/src/Attributes/Bootstrap.php +++ b/src/Attributes/Bootstrap.php @@ -9,7 +9,7 @@ use Drush\Boot\DrupalBootLevels; use JetBrains\PhpStorm\ExpectedValues; -#[Attribute(Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class Bootstrap { /** diff --git a/src/Commands/AutowireTrait.php b/src/Commands/AutowireTrait.php new file mode 100644 index 0000000000..1dc6b58880 --- /dev/null +++ b/src/Commands/AutowireTrait.php @@ -0,0 +1,48 @@ +getParameters() as $parameter) { + $service = ltrim((string) $parameter->getType(), '?'); + foreach ($parameter->getAttributes(Autowire::class) as $attribute) { + $service = (string) $attribute->newInstance()->value; + } + + if (!$container->has($service)) { + throw new AutowiringFailedException($service, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s::_construct()", you should configure its value explicitly.', $service, $parameter->getName(), static::class)); + } + + $args[] = $container->get($service); + } + } + + return new static(...$args); + } +} diff --git a/src/Commands/core/LoginCommands.php b/src/Commands/core/LoginCommands.php index 082249ba00..97cd16985d 100644 --- a/src/Commands/core/LoginCommands.php +++ b/src/Commands/core/LoginCommands.php @@ -4,44 +4,36 @@ namespace Drush\Commands\core; -use Drupal\Component\Datetime\TimeInterface; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\State\StateInterface; -use Drush\Attributes as CLI; +use Consolidation\SiteAlias\SiteAliasManagerAwareInterface; +use Consolidation\SiteAlias\SiteAliasManagerAwareTrait; +use Drupal\Core\Url; use Drupal\user\Entity\User; +use Drush\Attributes as CLI; use Drush\Boot\BootstrapManager; use Drush\Boot\DrupalBootLevels; +use Drush\Commands\AutowireTrait; use Drush\Commands\DrushCommands; use Drush\Drush; use Drush\Exec\ExecTrait; -use Consolidation\SiteAlias\SiteAliasManagerAwareInterface; -use Consolidation\SiteAlias\SiteAliasManagerAwareTrait; -use Drupal\Core\Url; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drush\Runtime\DependencyInjection; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +#[CLI\Bootstrap(level: DrupalBootLevels::NONE)] final class LoginCommands extends DrushCommands implements SiteAliasManagerAwareInterface { + use AutowireTrait; use SiteAliasManagerAwareTrait; use ExecTrait; const LOGIN = 'user:login'; - public function __construct(private BootstrapManager $bootstrapManager) - { + public function __construct( + #[Autowire(service: DependencyInjection::BOOTSTRAP_MANAGER)] + private BootstrapManager $bootstrapManager + ) { parent::__construct(); } - public static function createEarly($drush_container): self - { - $commandHandler = new static( - $drush_container->get('bootstrap.manager') - ); - - return $commandHandler; - } - /** * Display a one time login link for user ID 1, or another user. */ diff --git a/src/Commands/core/MaintCommands.php b/src/Commands/core/MaintCommands.php index 19282cfdb1..0ed789fed9 100644 --- a/src/Commands/core/MaintCommands.php +++ b/src/Commands/core/MaintCommands.php @@ -6,11 +6,13 @@ use Drupal\Core\State\StateInterface; use Drush\Attributes as CLI; +use Drush\Commands\AutowireTrait; use Drush\Commands\DrushCommands; -use Symfony\Component\DependencyInjection\ContainerInterface; final class MaintCommands extends DrushCommands { + use AutowireTrait; + const KEY = 'system.maintenance_mode'; const GET = 'maint:get'; const SET = 'maint:set'; @@ -18,15 +20,7 @@ final class MaintCommands extends DrushCommands public function __construct(protected StateInterface $state) { - } - - public static function create(ContainerInterface $container): self - { - $commandHandler = new static( - $container->get('state') - ); - - return $commandHandler; + parent::__construct(); } public function getState(): StateInterface diff --git a/src/Commands/sql/SqlCommands.php b/src/Commands/sql/SqlCommands.php index 6ccefd96fd..f3d91f5356 100644 --- a/src/Commands/sql/SqlCommands.php +++ b/src/Commands/sql/SqlCommands.php @@ -8,9 +8,10 @@ use Consolidation\AnnotatedCommand\Hooks\HookManager; use Consolidation\AnnotatedCommand\Input\StdinAwareInterface; use Consolidation\AnnotatedCommand\Input\StdinAwareTrait; +use Consolidation\OutputFormatters\StructuredData\PropertyList; use Consolidation\SiteProcess\Util\Tty; -use Drush\Attributes as CLI; use Drupal\Core\Database\Database; +use Drush\Attributes as CLI; use Drush\Boot\DrupalBootLevels; use Drush\Commands\core\DocsCommands; use Drush\Commands\DrushCommands; @@ -18,7 +19,6 @@ use Drush\Exceptions\UserAbortException; use Drush\Exec\ExecTrait; use Drush\Sql\SqlBase; -use Consolidation\OutputFormatters\StructuredData\PropertyList; use Symfony\Component\Console\Input\InputInterface; final class SqlCommands extends DrushCommands implements StdinAwareInterface @@ -86,7 +86,7 @@ public function connect($options = ['extra' => self::REQ]): string #[CLI\Usage(name: 'drush sql:create --db-su=root --db-su-pw=rootpassword --db-url="mysql://drupal_db_user:drupal_db_password@127.0.0.1/drupal_db"', description: 'Create the database as specified in the db-url option.')] #[CLI\Bootstrap(level: DrupalBootLevels::MAX, max_level: DrupalBootLevels::CONFIGURATION)] #[CLI\OptionsetSql] - public function create($options = ['db-su' => self::REQ, 'db-su-pw' => self::REQ]): void + public function createDb($options = ['db-su' => self::REQ, 'db-su-pw' => self::REQ]): void { $sql = SqlBase::create($options); $db_spec = $sql->getDbSpec(); diff --git a/src/Runtime/DependencyInjection.php b/src/Runtime/DependencyInjection.php index 65cb9e5219..f812a73a29 100644 --- a/src/Runtime/DependencyInjection.php +++ b/src/Runtime/DependencyInjection.php @@ -4,37 +4,40 @@ namespace Drush\Runtime; -use Drush\Formatters\EntityToArraySimplifier; -use Drush\Log\Logger; -use League\Container\Container; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\ConsoleOutput; -use Robo\Robo; -use Drush\Formatters\DrushFormatterManager; +use Composer\Autoload\ClassLoader; +use Consolidation\Config\ConfigInterface; +use Consolidation\Config\Util\ConfigOverlay; +use Consolidation\SiteAlias\SiteAliasManager; use Consolidation\SiteAlias\SiteAliasManagerAwareInterface; use Consolidation\SiteProcess\ProcessManagerAwareInterface; +use Drush\Cache\CommandCache; +use Drush\Command\DrushCommandInfoAlterer; use Drush\Command\GlobalOptionsEventListener; +use Drush\Config\DrushConfig; +use Drush\DrupalFinder\DrushDrupalFinder; use Drush\Drush; +use Drush\Formatters\DrushFormatterManager; +use Drush\Formatters\EntityToArraySimplifier; +use Drush\Log\Logger; +use Drush\SiteAlias\ProcessManager; use Drush\Symfony\DrushStyleInjector; -use Drush\Cache\CommandCache; -use Drush\DrupalFinder\DrushDrupalFinder; +use League\Container\Container; +use League\Container\ContainerInterface; +use Robo\Robo; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Application; -use Consolidation\Config\ConfigInterface; -use Composer\Autoload\ClassLoader; -use League\Container\ContainerInterface; -use Consolidation\SiteAlias\SiteAliasManager; -use Drush\Command\DrushCommandInfoAlterer; -use Consolidation\Config\Util\ConfigOverlay; -use Drush\Config\DrushConfig; -use Drush\SiteAlias\ProcessManager; /** * Prepare our Dependency Injection Container */ class DependencyInjection { + const BOOTSTRAP_MANAGER = 'bootstrap.manager'; + const LOADER = 'loader'; + const SITE_ALIAS_MANAGER = 'site.alias.manager'; protected array $handlers = []; public function desiredHandlers($handlerList): void @@ -103,8 +106,8 @@ protected function addDrushServices($container, ClassLoader $loader, DrushDrupal ->addMethodCall('setLogOutputStyler', ['logStyler']) ->addMethodCall('add', ['drush', new Logger($output)]); - Robo::addShared($container, 'loader', $loader); - Robo::addShared($container, 'site.alias.manager', $aliasManager); + Robo::addShared($container, self::LOADER, $loader); + Robo::addShared($container, self::SITE_ALIAS_MANAGER, $aliasManager); // Fetch the runtime config, where -D et. al. are stored, and // add a reference to it to the container. @@ -119,17 +122,17 @@ protected function addDrushServices($container, ClassLoader $loader, DrushDrupal // Add some of our own objects to the container Robo::addShared($container, 'service.manager', 'Drush\Runtime\ServiceManager') - ->addArgument('loader') + ->addArgument(self::LOADER) ->addArgument('config') ->addArgument('logger'); Robo::addShared($container, 'bootstrap.drupal8', 'Drush\Boot\DrupalBoot8') ->addArgument('service.manager') - ->addArgument('loader'); - Robo::addShared($container, 'bootstrap.manager', 'Drush\Boot\BootstrapManager') + ->addArgument(self::LOADER); + Robo::addShared($container, self::BOOTSTRAP_MANAGER, 'Drush\Boot\BootstrapManager') ->addMethodCall('setDrupalFinder', [$drupalFinder]) ->addMethodCall('add', ['bootstrap.drupal8']); Robo::addShared($container, 'bootstrap.hook', 'Drush\Boot\BootstrapHook') - ->addArgument('bootstrap.manager'); + ->addArgument(self::BOOTSTRAP_MANAGER); Robo::addShared($container, 'tildeExpansion.hook', 'Drush\Runtime\TildeExpansionHook'); Robo::addShared($container, 'process.manager', ProcessManager::class) ->addMethodCall('setConfig', ['config']) @@ -152,7 +155,7 @@ protected function addDrushServices($container, ClassLoader $loader, DrushDrupal // Add inflectors. @see \Drush\Boot\BaseBoot::inflect $container->inflector(SiteAliasManagerAwareInterface::class) - ->invokeMethod('setSiteAliasManager', ['site.alias.manager']); + ->invokeMethod('setSiteAliasManager', [self::SITE_ALIAS_MANAGER]); $container->inflector(ProcessManagerAwareInterface::class) ->invokeMethod('setProcessManager', ['process.manager']); } @@ -183,8 +186,8 @@ protected function alterServicesForDrush($container, Application $application): protected function injectApplicationServices($container, Application $application): void { $application->setLogger($container->get('logger')); - $application->setBootstrapManager($container->get('bootstrap.manager')); - $application->setAliasManager($container->get('site.alias.manager')); + $application->setBootstrapManager($container->get(self::BOOTSTRAP_MANAGER)); + $application->setAliasManager($container->get(self::SITE_ALIAS_MANAGER)); $application->setRedispatchHook($container->get('redispatch.hook')); $application->setTildeExpansionHook($container->get('tildeExpansion.hook')); $application->setDispatcher($container->get('eventDispatcher')); diff --git a/src/Runtime/ServiceManager.php b/src/Runtime/ServiceManager.php index 29a401bc60..ecdfb58e0a 100644 --- a/src/Runtime/ServiceManager.php +++ b/src/Runtime/ServiceManager.php @@ -13,6 +13,8 @@ use Consolidation\SiteProcess\ProcessManagerAwareInterface; use Drupal\Component\DependencyInjection\ContainerInterface as DrupalContainer; use DrupalCodeGenerator\Command\BaseGenerator; +use Drush\Attributes\Bootstrap; +use Drush\Boot\DrupalBootLevels; use Drush\Commands\DrushCommands; use Drush\Config\DrushConfig; use Grasmash\YamlCli\Command\GetValueCommand; @@ -105,9 +107,9 @@ public function discover(array $commandfileSearchpath, string $baseNamespace): a [FilterHooks::class] )); - // If a command class has a static `create` method, then we will + // If a command class has a Bootstrap Attribute or `static` create factory, // postpone instantiating it until after we bootstrap Drupal. - $this->bootstrapCommandClasses = array_filter($commandClasses, [$this, 'hasStaticCreateFactory']); + $this->bootstrapCommandClasses = array_filter($commandClasses, [$this, 'requiresBootstrap']); // Remove the command classes that we put into the bootstrap command classes. $commandClasses = array_diff($commandClasses, $this->bootstrapCommandClasses); @@ -313,11 +315,18 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain return !$reflection->isAbstract(); }); + // Prevent duplicate calls to delegate() by checking for state. + if ($container && !$drushContainer->has('state')) { + $drushContainer->delegate($container); + } foreach ($bootstrapCommandClasses as $class) { $commandHandler = null; try { - if ($container && $this->hasStaticCreateFactory($class)) { + if ($this->hasStaticCreateFactory($class) && $this->supportsCompoundContainer($class, $drushContainer)) { + // Hurray, this class is compatible with the container with delegate. + $commandHandler = $class::create($drushContainer); + } elseif ($container && $this->hasStaticCreateFactory($class)) { $commandHandler = $class::create($container, $drushContainer); } elseif (!$container && $this->hasStaticCreateEarlyFactory($class)) { $commandHandler = $class::createEarly($drushContainer); @@ -337,6 +346,16 @@ public function instantiateServices(array $bootstrapCommandClasses, DrushContain return $commandHandlers; } + /** + * Determine if the first parameter of the create method supports our container with delegate. + */ + protected function supportsCompoundContainer($class, $drush_container): bool + { + $reflection = new \ReflectionMethod($class, 'create'); + $hint = (string)$reflection->getParameters()[0]->getType(); + return is_a($drush_container, $hint); + } + /** * Check to see if the provided class has a static `create` method. * @@ -350,6 +369,30 @@ protected function hasStaticCreateFactory(string $class): bool return static::hasStaticMethod($class, 'create'); } + /* + * Does the provided class have a Bootstrap Attribute, indicating early loading. + */ + protected function hasBootStrapAttributeNone(string $class): bool + { + try { + $reflection = new \ReflectionClass($class); + if ($attributes = $reflection->getAttributes(Bootstrap::class)) { + $bootstrap = $attributes[0]->newInstance(); + return $bootstrap->level === DrupalBootLevels::NONE; + } + } catch (\ReflectionException $e) { + } + return false; + } + + /** + * Check whether a command class requires Drupal bootstrap. + */ + protected function requiresBootstrap(string $class): bool + { + return $this->hasStaticCreateFactory($class) && !$this->hasBootStrapAttributeNone($class); + } + /** * Check to see if the provided class has a static `createEarly` method. * diff --git a/sut/modules/unish/woot/src/Drush/Generators/ExampleGenerator.php b/sut/modules/unish/woot/src/Drush/Generators/ExampleGenerator.php index 9787c208e5..d41b14b06e 100644 --- a/sut/modules/unish/woot/src/Drush/Generators/ExampleGenerator.php +++ b/sut/modules/unish/woot/src/Drush/Generators/ExampleGenerator.php @@ -4,16 +4,12 @@ namespace Drupal\woot\Drush\Generators; -use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Extension\ModuleInstallerInterface; -use Drupal\Core\Extension\ThemeHandlerInterface; use DrupalCodeGenerator\Asset\AssetCollection as Assets; use DrupalCodeGenerator\Attribute\Generator; use DrupalCodeGenerator\Command\BaseGenerator; use DrupalCodeGenerator\GeneratorType; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drush\Commands\AutowireTrait; #[Generator( name: 'woot:example', @@ -24,6 +20,8 @@ )] class ExampleGenerator extends BaseGenerator { + use AutowireTrait; + /** * Illustrates how to inject a dependency into a Generator. */ @@ -33,15 +31,6 @@ public function __construct( parent::__construct(); } - public static function create(ContainerInterface $container): self - { - $commandHandler = new static( - $container->get('module_handler'), - ); - - return $commandHandler; - } - protected function generate(array &$vars, Assets $assets): void { $ir = $this->createInterviewer($vars);