Last week we updated most of the Drupal Plugin API content to recommend, and demonstrate, using PHP attributes instead of annotations. This week I’m back with another update doing the same thing for our content on creating custom Drush commands. Which now also use PHP attributes instead of annotations.
The big changes to this set of tutorials include:
- Command classes use attributes instead of annotations to provide context about a commands run-time requirements and features.
- Services are injected into command classes using autowiring and no longer require a separate drush.services.yml file.
When you run a Drush command like drush user:login
, Drush core routes the request to a specific method on a command class to perform the logic. These routes, and their features like command line arguments, options flags, and help text, are declared via metadata associated with the method to call. Previously, this was done using annotations, but as of Drush 12 you can (and should) use PHP attributes to provide information about a command’s name, arguments, options, and so forth.
Annotations are still supported, but unless you need to support backwards compatibility with older versions of Drush or PHP 7, it is recommended that you use attributes. Since our goal is to always provide instructions on the current best practices, we’ve updated the content accordingly.
While both annotations and native PHP attributes accomplish the same goal of providing metadata about the code being annotated, attributes are a language-level feature. As such, using attributes can result in a better developer experience, because, for example, your IDE can do syntax highlighting and autocompletion.
Before, using annotations:
/*
* Command that returns a list of all blocked users.
*
* @field-labels
* id: User id
* name: Username
* email: User email
* status: Status
* @default-fields id,name,email,status
*
* @param $status the string value of the user status on the site. Available values are: active and blocked
*
* @option show-status
* Show status of the user in the results
*
* @usage drush-helpers:blocked-users
* Returns all blocked users
*
* @command drush-helpers:blocked-users
* @aliases blocked-users
*
* @return ConsolidationOutputFormattersStructuredDataRowsOfFields
*/
public function blockedUsers($status = ‘blocked’) {}
After, using attributes:
/*
* Command that returns a list of all blocked users.
*
* @return ConsolidationOutputFormattersStructuredDataRowsOfFields
*/
#[CLICommand(name: 'drush_helpers:blocked-users', aliases: ['blocked-users'])]
#[CLIUsage(name: 'drush_helpers:blocked-users', description: 'Returns all blocked users')]
#[CLIArgument(name: 'status', description: 'The string value of the user status on the site. Available values are: active and blocked')]
#[CLIOption(name: 'show-status', description: 'Show status of the user in the results')]
#[CLIFieldLabels(labels: [
'id' => 'ID',
'name' => 'Username',
'email' => 'User email',
'status' => 'Status',
])]
#[CLIDefaultTableFields(fields: ['id', 'name', 'email', 'status'])]
public function blockedUsers($status = 'blocked', $options = ['show-status' => FALSE, 'format' => 'table']): RowsOfFields {}
One of the things that was new to me was the use of multiple attributes on a single method. I guess I knew this was possible because I read about it in the docs, but this was my first time seeing it in action. If, for example, your command takes multiple arguments, you would add an attribute declaration for each one.
Autowiring services
In addition to using PHP attributes, Drush now also supports autowiring service dependencies. That is, if you use the AutowireTrait
trait, you can inject the services your command depends on via type hinting arguments passed to the command classes’ constructor. This reduces a lot of boiler plate code, and removes the need to create a drush.services.yml file and declare it in your project’s composer.json.
Previously, to inject services into a command class, you needed to create a drush.services.yml file in the root of your project. Something like the following:
services:
search_api.commands:
class: Drupalsearch_apiCommandsSearchApiCommands
arguments: ['@entity_type.manager']
tags:
- { name: drush.command }
And the services named in the arguments
configuration would be passed as arguments to the class constructor.
Now, you can use the DrushCommandsAutowireTrait
trait, which will effectively inspect the type hints of the arguments to your class constructor, load the relevant service, and inject that.
Autowiring services is available as of Drush 13, and will be required starting with Drush 14 (the next major version).
Here’s an example:
namespace Drupaldrush_helpersDrushCommands;
use DrupalCoreEntityEntityTypeManagerInterface;
use DrushCommandsAutowireTrait;
use DrushCommandsDrushCommands;
/**
* A Drush commandfile.
*/
final class DrushHelpersCommands extends DrushCommands {
use AutowireTrait;
/**
* Constructs a DrushHelpersCommands object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct();
}
}
For more complex scenarios where the service can’t be identified from the type hint, you can use the #[Autowire] attribute to provide additional context. And if that still doesn’t work, like when you’re getting a service that is a factory for API clients, and you want to inject the actual client and not the factory, you can still use the common static create()
method that is used prominently throughout Drupal core for things like block plugins and controllers.
Learn more.
Updated tutorials
In order to account for these updates, and a few other minor changes, I walked through all of the tutorials on writing custom Drush commands following along with the existing instructions and making updates as needed. This resulted in updates to 8 different tutorials. Work that is supported by the paying members of Drupalize.Me.
Here’s a list of the tutorials from our Learn Drush: The Drupal Shell course that were updated as part of this ongoing effort to keep everything up-to-date: