Drupal 8 tip of the day: replace hook_drush_command() by a YAML file

Submitted by Frederic Marand on

One of the big trends during the Drupal 8 creation has been the replacement of info hooks by two main mechanisms: annotations, and YAML files. In that light, hook_drush_command(), as the CLI equivalent of the late hook_menu, replaced by various YAML files, looks just like a perfect candidate for replacement by a commands section in some mymodule.drush.yml configuration file. Turns out it is incredibly easy to achieve. Let's see how to kill some hundred lines of code real fast !

The goal

Just like its hook_menu() web counterpart, hook_drush_command() defines controllers, the main difference being that it maps them to CLI commands instead of web paths. Its structure is that of a very basic info hook: a nested array of strings and numbers, which maps neatly to a YAML structure. Consider the canonical sandwich example from Drush docs.

hook_drush_command mymodule.drush.yml
<?php
function sandwich_drush_command() {
 
$items = array();

 
// The 'make-me-a-sandwich' command.
 
$items['make-me-a-sandwich'] = array(
   
'description' => "Makes a delicious sandwich.",
   
'arguments' => array(
     
'filling' => 'The type of the sandwich (turkey, cheese, etc.). Defaults to ascii.',
    ),
   
'options' => array(
     
'spreads' => array(
       
'description' => 'Comma delimited list of spreads.',
       
'example-value' => 'mayonnaise,mustard',
      ),
    ),
   
'examples' => array(
     
'drush mmas turkey --spreads=ketchup,mustard' => 'Make a terrible-tasting sandwich that is lacking in pickles.',
    ),
   
'aliases' => array('mmas'),
   
// No bootstrap at all.
   
'bootstrap' => DRUSH_BOOTSTRAP_NONE,
  );
   
?>
commands:
  # The 'make-me-a-sandwich' command.
  make-me-a-sandwich:
    description' => "Makes a delicious sandwich."
    arguments:
      filling: 'The type of the sandwich (turkey, cheese, etc.). Defaults to ascii.'
    options:
      spreads:
        description: 'Comma delimited list of spreads.'
        example-value: 'mayonnaise,mustard'
    examples:
      'drush mmas turkey --spreads=ketchup,mustard': 'Make a terrible-tasting sandwich that is lacking in pickles.'
    aliases: ['mmas']
    # No bootstrap at all.
    bootstrap: -1
    

The trick

There is no trick ! Well, almost... assuming that simple format for the mymodule.drush.yml file, all it takes is loading it. This can even be entirely generic, like this :
<?php
use Symfony\Component\Yaml\Yaml;

/**
 * Implements hook_drush_command().
 */
function beanstalkd_drush_command() {
 
$file = preg_replace('/(inc|php)$/', 'yml', __FILE__);
 
$config = Yaml::parse(file_get_contents($file));
 
$items = $config['commands'];
  return
$items;
}
?>

Since your drush plugin for module mymodule is named mymodule.drush.inc (or mymodule.drush.php if you write them like me), the name of the Yaml file can be deduced from the plugin name. And then, the Symfony Yaml component parses it to a plain array matching the expected hook_drush_command() structure.

This is the mechanism used by the Drupal 8 version of the Beanstalkd module.

The limitations

You may have noticed a small limitation : hook_drush_command() implementations may need to use constants like DRUSH_BOOTSTRAP_NONE. With such a limited implementation, you will need to convert the constants to their values from drush/includes/bootstrap.inc. Should the mechanism come to be included in Drush at some point, a more evolved mechanism will likely provide translation for these constants, too.

phenaproxima (not verified)

Sun, 2015-10-25 20:08

This is an awesome idea, and one of the Drush maintainers agrees with me. Working on a patch now :)