Skip to main content

Creating a command using Drupal Console

Friday 13 de December de 2019

Introduction

Drupal Console eases Drupal 8 development in different ways, but the one we're interested in in in this article is code generation. In version 8 Drupal has become much more object-oriented, which is good, but in return it is more complex to do certain tasks that were previously done by implementing one or two hooks in the .module file. This is where Drupal Console comes in: it offers commands to generate code for many of the tasks you may need to develop.

Some examples of commands:

drupal generate:module [options] # Generates base code for a new module.
drupal generate:service [options] # Generates base code for a service.
drupal generate:form [options] # Generates base code for a form.

In this way you can generate code with the base for a new module, a new form (you can even tell him what form elements you want!) or a new Drupal service.

But in this article we will not talk about generating code, but create a new Drupal Console command for Drupal 8. Specifically, in this practical example we are going to create a Drupal Console command that will allow us to generate the code base for a QueueWorker. A QueueWorker is in charge of processing the elements of a Drupal 8 queue API queue. Let's imagine that our web requires to make a process on certain data, for example on data that are received from time to time from external sources. By means of the Queue API we can create a queue, insert the received data in this queue, and that later a QueueWorker would recover and process the data.

Drupal Console installation

First you'll need to install Drupal Console. As many (if not all) Drupal projects, we use Composer to add it as a dependency.

composer require drupal/console:~1.0 \
--prefer-dist \
--optimize-autoloader

You can install Drupal Console Launcher to execute Drupal Console globally without using the complete path (vendor/bin/drupal) on each project. Check the documentation.

Create a scaffolding for a command

To create our own Drupal Console command what better way to start than by using Drupal Console to generate the initial code. This is what is called scaffolding: the action of generating the code and directories needed to start a project or component.

Therefore we will use the following command:

drupal generate:command [options]

The command's options are:

--extension=EXTENSION   Extension name
--extension-type=EXTENSION-TYPE  Extension type (module, theme, profile).
--class=CLASS   The class that defines the commands (it should end on 'Command').
--name=NAME   The name of the command.
--initialize   Adds an initialize method.
--interact   Adds an interact command, in charge of processing input params.
--container-aware   Check a Drupal site is installed.
--services[=SERVICES]   Adds services.
--generator   Adds a Generator class in charge of handling the scaffolding.

Options are not mandatory, if not provided Drupal Console asks for them when run.

In this case we want to generate a command in the 'drupal_console_queue' extension of type module. We'll use a class called PluginQueueWorkerCommand, and the command will be generate:plugin:queue. The command line would be:

drupal generate:command  \
  --extension="drupal_console_queue"  \
  --extension-type="module"  \
  --class="PluginQueueWorkerCommand"  \
  --name="generate:plugin:queue" \
  --generator \
  --interact \
  --container-aware

This will generate a bunch of files and directories with the base for the command. Something like this:

Drupal console command module directory.

 

Let's check the generated module files according to their importance.

The main class is PluginQueueWorkerCommand, as it will be in charge of registering the Drupal command, that is, informing Drupal Console of the existence of a custom command. It will be located inside the /src/Command/Generate folder as it is a code generation command. This class extends Drupal\Console\Annotations\DrupalCommand and has an annotation to register it in Drupal Console, besides importing Drupal\Console\Annotations\DrupalCommand through the PHP use clause.

The generated code will be something like this:

namespace Drupal\drupal_console_queue\Command\Generate;

use Drupal\Console\Annotations\DrupalCommand;

/**
 * Class PluginQueueWorkerCommand.
 *
 * @DrupalCommand (
 *     extension="drupal_console_queue",
 *     extensionType="module"
 * )
 */
class PluginQueueWorkerCommand extends ContainerAwareCommand {

To inject services we need to create a console.services.yml file in module root directory. Here's an example to inject some services in the command class and the generator class.

services:
  drupal_console_queue.generate_queue:
    class: Drupal\drupal_console_queue\Command\Generate\PluginQueueWorkerCommand
    arguments: ['@drupal_console_queue.plugin_queue_generator','@console.validator','@console.string_converter','@console.chain_queue']
    tags:
      - { name: drupal.command }
  drupal_console_queue.plugin_queue_generator:
    class: Drupal\drupal_console_queue\Generator\PluginQueueWorkerGenerator
    arguments: ['@console.extension_manager']
    tags:
      - { name: drupal.generator }

Back to the PluginQueueWorkerCommand class, we'll define the injected services in the class constructor. In this case, we inject the generator that's in charge of generating the scaffolding code, the validator that helps to validate the user input, and the stringConverter to deal with the text.

...
   * @param \Drupal\Console\Core\Utils\ChainQueue $chainQueue
   *   Chain queue.
   */
  public function __construct(
    GeneratorInterface $queue_generator,
    Validator $validator,
    StringConverter $stringConverter
  ) {
    $this->generator = $queue_generator;
    $this->validator = $validator;
    $this->stringConverter = $stringConverter;
    parent::__construct();
  }

The configure method provides an alias to the Drupal Console command and add the available options.

  /**
   * {@inheritdoc}
   */
  protected function configure() {
    $this
      ->setName('generate:plugin:queue')
      ->setDescription($this->trans('commands.generate.plugin.queue.description'))
      ->setHelp($this->trans('commands.generate.plugin.queue.help'))
      ->addOption(
          'module',
          NULL,
          InputOption::VALUE_REQUIRED,
          $this->trans('commands.generate.plugin.queue.options.module')
      )
...
          'label',
          NULL,
          InputOption::VALUE_REQUIRED,
          $this->trans('commands.generate.plugin.queue.options.label')
      )
      ->setAliases(['gpqueue']);
  }

The command name can be added with the setName method.

Each option of the command is added with addOption method, its arguments are the argument name, an optional shortcut, input type (VALUE_NONE, VALUE_REQUIRED, VALUE_OPTIONAL, VALUE_IS_ARRAY) and the last argument corresponds to the help description for the command.

You can type the text directly, but if you want the command to be available in several languages you should use the trans method using commands.filename.option as an argument and add the translation yml file inside the folder drupal_console_queue/console/translations/<language> (in/en/fr...) with the translation.

For example, for this module we have the generate.plugin.queue.yml file placed in  drupal_console_queue/console/translations/en.

description: 'Drupal Console Queueworker generator.'
help: 'The <info>generate:plugin:queue</info> command helps you generate a new queue worker plugin.'
welcome: 'Welcome to the Drupal Queue Worker Plugin generator'
options:
  module: 'The module name'
...

Also, using the setAliases method we can define an alias for the Drupal Console command. The parameter is an array so more than one can be defined.

On the other hand you can create the interact method where you process each argument when launching the command. If your command does not need to interact with the user, this method is not needed.

  /**
   * {@inheritdoc}
   */
  protected function interact(InputInterface $input, OutputInterface $output) {
    // --module option.
    $this->getModuleOption();

    // --class option.
    $queue_class = $input->getOption('class');
    if (!$queue_class) {
      $queue_class = $this->getIo()->ask(
            $this->trans('commands.generate.plugin.queue.questions.class'),
            'ExampleQueue',
            function ($queue_class) {
              return $this->validator->validateClassName($queue_class);
            }
        );
      $input->setOption('class', $queue_class);
    }
...

In the example shown above we get the option class, and if it's not defined (because the user has not provided any options when executing the command), then we ask the users using the console with the ask method. The arguments of the ask method are the question that will be asked to the user (if we use the trans method and we have to define the translation in the yml file mentioned previously), the default value, and an input validation function that must be a callable (a function).

Once we get the answer from the user, we process it as needed. The option is defined with the value obtained with the setOption method, being the first argument the name of the option and the second argument the value of the option.

The last and most important method is the execute method. It is the method that actually performs the command actions.


  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output) {
    // @see use Drupal\Console\Command\Shared\ConfirmationTrait::confirmOperation
    if (!$this->confirmOperation()) {
      return 1;
    }
    $module = $input->getOption('module');
...
    $this->generator->generate([
      'module' => $module,
...
    ]);


    return 0;
  }

 

In our case we want to generate the scaffolding for a QueueWorker, so we pass the arguments from the command to the generator.

Let's now check the class in charge og generating the scaffolding code, the PluginQueueWorkerGenerator class. It's located inside the src/Generator folder. 

Generator directory QueueWorker Generator

This class should extend Drupal\Console\Core\Generator\Generator and implement the Drupal\Console\Core\Generator\GeneratorInterface. It should be part of the Drupal\Console\Generator package as well.

<?php

namespace Drupal\drupal_console_queue\Generator;

use Drupal\Console\Core\Generator\Generator;
use Drupal\Console\Core\Generator\GeneratorInterface;
use Drupal\Console\Extension\Manager;

/**
 * Class QueueWorkerGenerator.
 *
 * @package Drupal\Console\Generator
 */
class PluginQueueWorkerGenerator extends Generator implements GeneratorInterface {

As we did in the commands class, if we have defined some service in the console.services.yml file we must define it in the constructor. In this case we have injected the extension manager.

  /**
   * Extension Manager.
   *
   * @var \Drupal\Console\Extension\Manager
   */
  protected $extensionManager;

  /**
   * PluginQueueWorker constructor.
   *
   * @param \Drupal\Console\Extension\Manager $extensionManager
   *   Extension manager.
   */
  public function __construct(
       Manager $extensionManager
   ) {
    $this->extensionManager = $extensionManager;
  }

The only method that the interface forces us to implement is the generate method, which is in charge of generating the scaffolding code.

In this case we get the options of the module where we want to generate the scaffolding code and the name of the class for the QueueWorker.

  /**
   * {@inheritdoc}
   */
  public function generate(array $parameters) {
    $module = $parameters['module'];
    $queue_class = $parameters['class_name'];
...

Later we define the directory where the template is with $this->renderer->addSkeletonDir. It needs the template twig location as parameter since the generation of scaffolding is done using a twig template.

The next step is to generate the file with the method $this->renderFile. The first argument is the path relative to the template folder. The second argument is file path and name of the generated file. Using the Extension Manager we get module path where the plugins should be located. The last argument will be the variables we want to use in twig.

...
    $this->renderer->addSkeletonDir(__DIR__ . '/../../console/templates');
    $this->renderFile(
      'module/src/Plugin/QueueWorker/queue_worker.php.twig',
      $this->extensionManager->getPluginPath($module, 'QueueWorker') . '/' . $queue_class . '.php',
      $parameters
    );
  }

We create the twig in the drupal_console_queue/console/templates/module/src/Plugin/QueueWorker/queue_worker.php file.

Template twig QueueWorker

 

And it's ready! This is you need to create a Drupal Console command the creates a QueueWorker plugin. From this you can create other Drupal Console generator command or adapt this example to the code you want to generate.

This example has been contributed to the Drupal Console, here you can check the Pull Requests and see all the needed code. They are already commited to Drupal Console :)

Pull request for the Drupal Console QueueWorker Generator.

Pull requestfor the Drupal Console QueueWorker Generator translation.

Related links

Install Drupal Console.

Create a custom Drupal Console command.

Repository with the code of this article.