Skip to content

Commit 2cf879e

Browse files
authored
Use Autowire in commandfiles (#5889)
1 parent 39e5592 commit 2cf879e

File tree

78 files changed

+537
-773
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+537
-773
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"squizlabs/php_codesniffer": "^3.7"
6565
},
6666
"conflict": {
67-
"drupal/core": "< 10.0",
67+
"drupal/core": "< 10.1",
6868
"drupal/migrate_run": "*",
6969
"drupal/migrate_tools": "<= 5"
7070
},

composer.lock

Lines changed: 17 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/bootstrap.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Bootstrapping is done from a Symfony Console command hook. The different bootstr
4343

4444
none
4545
-----------------------
46-
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.
46+
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.
4747

4848
root
4949
------------------------------
@@ -68,4 +68,3 @@ Fully initialize Drupal. This is analogous to the DRUPAL\_BOOTSTRAP\_FULL bootst
6868
max
6969
---------------------
7070
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.
71-

docs/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
!!! tip
44

5-
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.
5+
1. Drush 13 expects commandfiles to use [Autowire](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php) 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.
66
1. Drush 12 expects all commandfiles in the `<module-name>/src/Drush/<Commands|Generators>` directory. The `Drush` subdirectory is a new requirement.
77

88
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.
1111
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.
1212
3. Drush will then report that it created a commandfile. Edit as needed.
1313
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.
14-
5. See the [dependency injection docs](dependency-injection.md) for interfaces you can implement to gain access to Drush config, Drush site aliases, etc. Also note the [create() method](dependency-injection.md#create-method) for injecting Drupal or Drush dependencies.
14+
5. You may [inject dependencies](dependency-injection.md) into a command instance.
1515
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).
1616

1717
## Attributes or Annotations

docs/dependency-injection.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ Dependency Injection
33

44
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.
55

6-
create() method
6+
!!! tip
7+
8+
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 removed in Drush 13.
9+
10+
Autowire
711
------------------
8-
:octicons-tag-24: 11.6+
12+
:octicons-tag-24: 13
13+
Command files may inject Drush and Drupal services by adding the [AutowireTrait](https://github.com/drush-ops/drush/blob/13.x/src/Commands/AutowireTrait.php) to the class (example: [PmCommands](https://github.com/drush-ops/drush/blob/13.x/src/Commands/pm/PmCommands.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/13.x/src/Commands/core/LoginCommands.php)). This Attribute is currently _required_ when injecting Drush services (not required for Drupal services).
914

10-
!!! tip
15+
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).
1116

12-
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.
17+
create() method
18+
------------------
19+
:octicons-tag-24: 11.6+
1320

14-
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:
21+
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:
1522
```php
1623
class WootStaticFactoryCommands extends DrushCommands
1724
{
@@ -22,33 +29,27 @@ class WootStaticFactoryCommands extends DrushCommands
2229
$this->configFactory = $configFactory;
2330
}
2431

25-
public static function create(ContainerInterface $container, DrushContainer $drush): self
32+
public static function create(ContainerInterface $container): self
2633
{
2734
return new static($container->get('config.factory'));
2835
}
2936
```
30-
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.
31-
32-
Note that if you do not need to pull any services from the Drush container, then you may
33-
omit the second parameter to the `create()` method.
37+
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.
3438

3539
createEarly() method
3640
------------------
3741
:octicons-tag-24: 12.0+
38-
Drush commands that need to be instantiated prior to bootstrap may do so by
39-
utilizing the `createEarly()` static factory. This method looks and functions
40-
exacty like the `create()` static factory, except it is only passed the Drush
41-
container. The Drupal container is not available to command handlers that use
42-
`createEarly()`.
42+
!!! tip
43+
44+
Drush 12 supported a createEarly() method. This is deprecated and 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
45+
until after Drupal bootstraps, and therefore cannot use `createEarly()`. This
46+
mechanism is only usable by PSR-4 discovered commands packaged with Composer
47+
projects that are *not* Drupal modules.
4348

44-
Note also that Drush commands packaged with Drupal modules are not discovered
45-
until after Drupal bootstraps, and therefore cannot use `createEarly()`. This
46-
mechanism is only usable by PSR-4 discovered commands packaged with Composer
47-
projects that are *not* Drupal modules.
4849

4950
Inflection
5051
-----------------
5152
A command class may implement the following interfaces. When doing so, implement the corresponding trait to satisfy the interface.
5253

5354
- [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)
54-
- [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)
55+
- [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)

docs/generators.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
!!! tip
44

5-
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.
5+
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.
66

77
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.
88

@@ -17,7 +17,7 @@ Creating a new Drush generator is easy. Follow the steps below.
1717
2. Drush will prompt for the machine name of the module that should _own_ the files. The module selected must already exist and be enabled. Use `drush generate module` to create a new module.
1818
3. Drush will then report that it created a generator (PHP class and twig file). Edit as needed.
1919
4. Similar to [ExampleGenerator](https://github.com/drush-ops/drush/tree/12.x/sut/modules/unish/woot/src/Drush/Generators), implement your custom logic in the generate() method.
20-
5. See the [dependency injection docs](dependency-injection.md) for interfaces you can implement to gain access to Drush config, Drush site aliases, etc. Also note the [create() method](dependency-injection.md#create-method) for injecting Drupal or Drush dependencies.
20+
5. You may [inject dependencies](dependency-injection.md) from Drupal or Drush.
2121

2222
## Auto-discovered Generators (PSR4)
2323

docs/hooks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ All commandfiles may implement methods that are called by Drush at various times
99

1010
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)
1111

12-
First, the command must implement CustomEventAwareInterface and use CustomEventAwareTrait, as described in the [dependency injection](dependency-injection.md) documentation.
12+
First, the command must implement CustomEventAwareInterface and use CustomEventAwareTrait, as described in the [dependency injection](dependency-injection.md#inflection) documentation.
1313

1414
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:
1515
```php

docs/install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Drupal Compatibility
3131
<td> Drush 13 </td>
3232
<td> 8.3+ </td>
3333
<td> TBD </td>
34-
<td></td> <td></td> <td></td> <td><b>✓</b></td> <td><b>✅</b></td>
34+
<td></td> <td></td> <td></td> <td><b>✓ 10.2+</b></td> <td><b>✅</b></td>
3535
</tr>
3636
<tr>
3737
<td> Drush 12 </td>

docs/site-alias-manager.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ Site Alias Manager
44
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.
55

66
- An informative example is the [browse command](https://github.com/drush-ops/drush/blob/12.x/src/Commands/core/BrowseCommands.php)
7-
- A commandfile gets access to the SAM by implementing the SiteAliasManagerAwareInterface and *use*ing the SiteAliasManagerAwareTrait trait. Then you gain access via `$this->siteAliasManager()`.
8-
- If an alias was used for the current request, it is available via `$this->siteAliasManager()->getself()`.
7+
- A commandfile gets access to the SAM as follows:
8+
```php
9+
#[Autowire(service: DependencyInjection::SITE_ALIAS_MANAGER)]
10+
private readonly SiteAliasManagerInterface $siteAliasManager
11+
```
12+
- If an alias was used for the current request, it is available via `$this->siteAliasManager->getself()`.
913
- 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.
1014
- [Site alias docs](site-aliases.md).
1115
- [Dynamically alter site aliases](https://raw.githubusercontent.com/drush-ops/drush/11.x/examples/Commands/SiteAliasAlterCommands.php).

examples/Commands/SiteAliasAlterCommands.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,26 @@
66
use Consolidation\AnnotatedCommand\Hooks\HookManager;
77
use Consolidation\SiteAlias\SiteAliasManagerInterface;
88
use Drush\Attributes as CLI;
9-
use League\Container\Container as DrushContainer;
9+
use Drush\Boot\DrupalBootLevels;
10+
use Drush\Runtime\DependencyInjection;
1011
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
1113

1214
/**
1315
* Load this example by using the --include option - e.g. `drush --include=/path/to/drush/examples`
1416
*/
17+
#[CLI\Bootstrap(DrupalBootLevels::NONE)]
1518
class SiteAliasAlterCommands extends DrushCommands
1619
{
20+
use AutowireTrait;
21+
1722
public function __construct(
23+
#[Autowire(service: DependencyInjection::SITE_ALIAS_MANAGER)]
1824
private readonly SiteAliasManagerInterface $siteAliasManager
1925
) {
2026
parent::__construct();
2127
}
2228

23-
public static function createEarly(DrushContainer $drush_container): self
24-
{
25-
$commandHandler = new static(
26-
$drush_container->get('site.alias.manager'),
27-
);
28-
29-
return $commandHandler;
30-
}
31-
3229
/**
3330
* A few example alterations to site aliases.
3431
*/

src/Application.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
use Drush\Command\RemoteCommandProxy;
1313
use Drush\Config\ConfigAwareTrait;
1414
use Drush\Runtime\RedispatchHook;
15-
use Drush\Runtime\TildeExpansionHook;
1615
use Drush\Runtime\ServiceManager;
16+
use Drush\Runtime\TildeExpansionHook;
1717
use Psr\Log\LoggerAwareInterface;
1818
use Psr\Log\LoggerAwareTrait;
1919
use Robo\Contract\ConfigAwareInterface;

src/Attributes/Bootstrap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Drush\Boot\DrupalBootLevels;
1010
use JetBrains\PhpStorm\ExpectedValues;
1111

12-
#[Attribute(Attribute::TARGET_METHOD)]
12+
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
1313
class Bootstrap
1414
{
1515
/**

src/Commands/AutowireTrait.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Drush\Commands;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
6+
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
7+
8+
/**
9+
* A copy of \Drupal\Core\DependencyInjection\AutowireTrait with first params' type hint changed.
10+
*
11+
* Defines a trait for automatically wiring dependencies from the container.
12+
*
13+
* This trait uses reflection and may cause performance issues with classes
14+
* that will be instantiated multiple times.
15+
*/
16+
trait AutowireTrait
17+
{
18+
/**
19+
* Instantiates a new instance of the implementing class using autowiring.
20+
*
21+
* @param \Psr\Container\ContainerInterface $container
22+
* The service container this instance should use.
23+
*
24+
* @return static
25+
*/
26+
public static function create(\Psr\Container\ContainerInterface $container)
27+
{
28+
$args = [];
29+
30+
if (method_exists(static::class, '__construct')) {
31+
$constructor = new \ReflectionMethod(static::class, '__construct');
32+
foreach ($constructor->getParameters() as $parameter) {
33+
$service = ltrim((string) $parameter->getType(), '?');
34+
foreach ($parameter->getAttributes(Autowire::class) as $attribute) {
35+
$service = (string) $attribute->newInstance()->value;
36+
}
37+
38+
if (!$container->has($service)) {
39+
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));
40+
}
41+
42+
$args[] = $container->get($service);
43+
}
44+
}
45+
46+
return new static(...$args);
47+
}
48+
}

src/Commands/DrushCommands.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use Robo\Contract\ConfigAwareInterface;
2525
use Robo\Contract\IOAwareInterface;
2626
use Symfony\Component\Console\Input\InputOption;
27-
use Symfony\Component\Console\Style\SymfonyStyle;
2827
use Symfony\Component\Filesystem\Path;
2928

3029
abstract class DrushCommands implements IOAwareInterface, LoggerAwareInterface, ConfigAwareInterface, ProcessManagerAwareInterface

0 commit comments

Comments
 (0)