Upgrading to Drush 9
Drush should be installed and updated through composer. There is no stable Drush 9 version yet, so the development version must be used. Updating to the development version of Drush 9 is a simple as typing:
$ composer require drush/drush:dev-master
Porting your Drush commands to Drush 9
Porting the commands is a semi-automatic process: There is a command that will generate the required files and class structure for you. To start the wizard, just type:
$ drush generate drush-command-file -l dev
Drush will ask you for the module's machine name and for the optional path to the legacy Drush command file (the one that has your commands, ending with .drush.inc). You will have to provide the absolute path.
drush.services.yml
This is the file your Drush command definition goes into. Do not use your module's regular services.yml as you might have done in Drush 8 or else you will confuse the legacy Drush which will lead to a PHP error like this:
Fatal error: Class 'Drush\Commands\DrushCommands' not found in MyModuleCommands.
Use the dedicated drush.services.yml file in your module's root directory instead.
The file should look like this:
services: mymodule.commands: class: \Drupal\mymodule\Commands\MyModuleCommands tags: - { name: drush.command }
As in other symfony service definitions, you can (and should) provide other services as arguments DI style and do all the other crazy stuff.
The most recent Drush 9 version recommends to explicitly declare the location of the drush command file for each version of drush by adding the extra.drush.services section to the composer.json file of the implementing module. This is now optional, but will be required for Drush 10.
To comply, let us declare the above file in composer.json for Drush 9:
"extra": { "drush": { "services": { "drush.services.yml": "^9" } } }
Refusing to alter composer.json will result in the following message while running drush commands:
module_name should have an extra.drush.services section. In the future, this will be required in order to use this Drush extension.
MyModuleCommands.php
namespace Drupal\mymodule\Commands; use Drush\Commands\DrushCommands; /** * * In addition to a commandfile like this one, you need a drush.services.yml * in root of your module. * * See these files for an example of injecting Drupal services: * - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php * - http://cgit.drupalcode.org/devel/tree/drush.services.yml */ class MyModuleCommands extends DrushCommands { /** * @command mymodule:do-something * @param array $options An associative array of options whose values come from cli, aliases, config, etc. * @validate-module-enabled mymodule * @aliases mm:do-something, mm:ds, mymodule-do-something */ public function generate() { // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a // legacy command. } }
As seen above, the generate() method needs to be implemented manually. Other manual changes may include creating a constructor in case other services are injected.
Drush 9 mimics symfony's style module:command naming structure and this should be respected. I don't see any reson not to include the legacy command as an alias however: If your command used to be my_module:do-something, use my-module:do-something in @command, but also the old my_module-do-something as @alias as presented in the example above. This way scripts calling the old Drush will continue working.
Maintaining Drush 8, Drush 9 and Drupal Console commands side by side
The new three standards of managing Drupal through a shell should not be an excuse for bad practice. To avoid code duplication, make sure your module defines a service which holds all the business logic that can be run by any of the above tools.
Simple XML Sitemap (project page) now supports Drush 9 and is a good example of this principle:
simple_sitemap.drush.inc (Drush 8)
/** * @file * Drush (< 9) integration. */ /** * Implements hook_drush_command(). */ function simple_sitemap_drush_command() { $items['simple-sitemap-generate'] = [ 'description' => 'Regenerate the XML sitemaps according to the module settings.', 'callback' => 'drush_simple_sitemap_generate', 'drupal dependencies' => ['simple_sitemap'], 'aliases' => ['ssg'], ]; $items['simple-sitemap-rebuild-queue'] = [ 'description' => 'Rebuild the sitemap queue for all sitemap variants.', 'callback' => 'drush_simple_sitemap_rebuild_queue', 'drupal dependencies' => ['simple_sitemap'], 'aliases' => ['ssr'], ]; return $items; } /** * Callback function for hook_drush_command(). * * Regenerate the XML sitemaps according to the module settings. */ function drush_simple_sitemap_generate() { \Drupal::service('simple_sitemap.generator')->generateSitemap('drush'); } /** * Callback function for hook_drush_command(). * * Rebuild the sitemap queue for all sitemap variants. */ function drush_simple_sitemap_rebuild_queue() { \Drupal::service('simple_sitemap.generator')->rebuildQueue(); }
SimplesitemapCommands.php (Drush 9)
namespace Drupal\simple_sitemap\Commands; use Drupal\simple_sitemap\Simplesitemap; use Drush\Commands\DrushCommands; /** * Class SimplesitemapCommands * @package Drupal\simple_sitemap\Commands */ class SimplesitemapCommands extends DrushCommands { /** * @var \Drupal\simple_sitemap\Simplesitemap */ protected $generator; /** * SimplesitemapCommands constructor. * @param \Drupal\simple_sitemap\Simplesitemap $generator */ public function __construct(Simplesitemap $generator) { $this->generator = $generator; } /** * Regenerate the XML sitemaps according to the module settings. * * @command simple-sitemap:generate * * @usage drush simple-sitemap:generate * Regenerate the XML sitemaps according to the module settings. * * @validate-module-enabled simple_sitemap * * @aliases ssg, simple-sitemap-generate */ public function generate() { $this->generator->generateSitemap('drush'); } /** * Rebuild the sitemap queue for all sitemap variants. * * @command simple-sitemap:rebuild-queue * * @usage drush simple-sitemap:rebuild-queue * Rebuild the sitemap queue for all sitemap variants. * * @validate-module-enabled simple_sitemap * * @aliases ssr, simple-sitemap-rebuild-queue */ public function rebuildQueue() { $this->generator->rebuildQueue(); } }
drush.services.yml (Drush 9)
services: simple_sitemap.commands: class: \Drupal\simple_sitemap\Commands\SimplesitemapCommands arguments: - '@simple_sitemap.generator' tags: - { name: drush.command }
All of the business logic of this command is inside of the method generateSitemap() of the simple_sitemap service.
Downgrading back to Drush 8
Not a fan of changing APIs? Downgrading is a composer command away:
$ composer require drush/drush:^8.0
Conclusion
It is good to see the Drush project keeping up with time and pubishing Drush 9 parallely to the appearance of Drupal 8.4.0. The API changes are the necessary price we pay for a modern and continuously evolving framework like Drupal.
Feel free to leave a comment below in case of questions or new Drupal 8.4 / Drush 9 insights.
Comments
Hi thanks for the article. By the way you miss to include the @simple_sitemap.generator as an argument in your drush.services while it is present in the command controller (and in the repo :)).
Thanks for your input, I've added the code for drush.services.yml to the article.
Add new comment