Pasar al contenido principal

Creando un comando con Drupal Console

Viernes 13 de Diciembre de 2019

Introducción

Drupal Console te facilita el desarrollo en Drupal 8 de diferentes maneras, pero la que nos interesa en este artículo es la generación de código. En la versión 8 Drupal se ha vuelto mucho más orientado a objetos, lo que es bueno, pero como contrapartida resulta más complejo hacer ciertas tareas que antes se hacían implementando uno o dos hooks el fichero .module. Es aquí donde interviene Drupal Console: ofrece comandos para generar código para muchas las tareas que puedes necesitar al desarrollar.

Los comandos que nos pueden resultar más intuitivos son:

drupal generate:module [options] # Para generar la base de un módulo.
drupal generate:service [options] # Para generar un servicio
drupal generate:form [options] # Para generar un formulario

De esta forma se puede generar código con la base para un nuevo módulo, un nuevo formulario (¡puedes incluso indicarle qué elementos de formulario quieres!) o  un nuevo servicio de Drupal. Imprescindible.

Pero en este artículo no se hablará de generar código, sino crear un nuevo comando de Drupal Console para Drupal 8. Concretamente, en este ejemplo práctico se va a crear un comando de Drupal Console que nos permita generar la base de código para un QueueWorker del que podamos partir en futuros desarrollos en Drupal 8. Un QueueWorker es quien se encarga de procesar los elementos de una cola de la API de colas de Drupal 8. Es decir, imaginemos que nuestra web requiere hacer un proceso sobre ciertos datos, por ejemplo sobre datos que se reciben de vez en cuando de fuentes externas. Mediante al API de colas podemos crear una cola, insertar los datos recibidos en dicha cola, y que posteriormente un QueueWorker vaya recuperando los datos y realizando el procesado requerido.

Instalación de Drupal Console

Lo primero es instalar Drupal Console. Al igual que la mayoría de proyectos de Drupal hay que añadirlo mediante Composer cómo dependencia para poder usarlo.

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

Existe la opción de instalar Drupal Console Launcher para ejecutar el Drupal Console globalmente sin necesidad de escribir vendor/bin/drupal en cada proyecto que necesitemos usarlo. Puedes ver cómo hacerlo en su documentación.

Crear un scaffolding de un comando

Para crear nuestro propio comando de Drupal Console qué mejor manera de empezar que usando Drupal Console para generar el código inicial. Esto es lo que se llama scaffolding: término en inglés para referirse a la acción de generar el código y los directorios necesarios para empezar un proyecto o componente.

Por tanto usaremos el siguiente comando:

drupal generate:command [options]

Las opciones de este comando son:

--extension=EXTENSION                   Nombre de la extensión, puede ser un módulo, tema o profile existente.
--extension-type=EXTENSION-TYPE  Tipo de extensión, (module, theme, profile).
--class=CLASS                                  La clase de define el comando. (Debe acabar por 'Command').
--name=NAME                                  El nombre del comando.
--initialize                                         Añadir el método de initialize.
--interact                                          Añade el método de interact, encargado de procesar los parámetros del comando.
--container-aware                             Hacer la comprobación de que un sitio drupal está instalado.
--services[=SERVICES]                     Añade servicios.
--generator                                       Añade la clase Generator encargada de gestionar el scaffolding.

Las opciones no son obligatorias, si no se rellenan, al lanzar el comando te las va pidiendo. 

 

En  nuestro caso queremos generar un comando dentro de una extensión llamada «drupal_console_queue» de tipo módulo, usando una clase llamada PluginQueueWorkerCommand, y el comando se llamará generate:plugin:queue. Así que usaremos la siguiente orden:

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

Esto nos generará unos cuantos ficheros y directorios con la base de nuestro comando. Nos quedará algo como esto:

Drupal console command module directory.

Vamos a revisar los ficheros del módulo creado según su importancia.

La principal clase es la de PluginQueueWorkerCommand, ya que será la encargada de registrar el comando de Drupal, es decir, de informar a Drupal Console de la existencia de un comando personalizado. Estará situada dentro de la carpeta /src/Command/Generate ya que es un comando de generación de código. Esta clase extiende Drupal\Console\Annotations\DrupalCommand y tiene una anotación para registrarlo en Drupal Console, además de importar Drupal\Console\Annotations\DrupalCommand mediante la cláusula use de PHP.

El código generado será algo así:

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 {

 

Para inyectar servicios deberemos definir previamente el fichero console.services.yml en la raíz del módulo, aquí se muestra un ejemplo de cómo inyectar varios servicios tanto en la clase del command como en el generator.

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 }

De vuelta a la clase PluginQueueWorkerCommand en el constructor definiremos los servicios que hemos inyectado a través del fichero previo.

En este caso vamos a inyectar el generator que se encargará de generar el scaffolding, el validator que nos ayudará a validar la entrada del usuario y el servicio stringConverter para poder convertir texto.

...
   * @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();
  }

 

El método configure servirá para darle un alias al comando de Drupal Console y añadir las opciones disponibles.

  /**
   * {@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']);
  }

El nombre del comando se puede añadir con el método setName.

Cada opción del comando se añade con addOption, sus argumentos son el nombre de argumento, el atajo si se quiere definir, tipo de entrada (VALUE_NONE, VALUE_REQUIRED, VALUE_OPTIONAL, VALUE_IS_ARRAY) y el último argumento corresponde a la descripción de esa opción que saldrá cuando lo ejecutemos como una ayuda.

Se puede meter el texto directamente, pero si queremos que el comando esté disponibles en varios idiomas debemos usar el método trans usando como argumento commands.nombrefichero.opción y añadiremos el fichero yml de la traducción dentro de la carpeta drupal_console_queue/console/translations/<idioma> (en/es/fr...) con la traducción.

Ejemplo, fichero generate.plugin.queue.yml situado en 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'
...

Además usando el método setAliases podemos definir un alias para el comando de Drupal Console, se pasará cómo array por lo que se podrá definir más de uno.

Por otro lado se puede crear el método interact donde se definirá el tratamiento que se hará con cada argumento al lanzar el comando.
Si tu comando no necesita interactuar con el usuario este método no será necesario que se defina.

  /**
   * {@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);
    }
...

En el ejemplo que se muestra previamente obtenemos la opción class, y si no ha sido definida porque el usuario no lo ha hecho en el momento de ejecutar el comando, por ejemplo definiendo el argumento (drupal generate:plugin:queue --class ExampleClass), entonces se lo preguntamos por consola con el método ask. Los argumentos del método ask son la pregunta que se le hará al usuario(usaremos el método trans y deberemos definir la traducción en el fichero yml que hemos comentado anteriormente), el valor por defecto, y una función de validación de entrada que debe ser un callable (una función).

Obtenemos el resultado de la pregunta, haremos el tratamiento que se desee con el resultado obtenido. Se definirá la opción con el valor obtenido con el método setOption, siendo el primer argumento el nombre de la opción y el segundo argumento el valor de la opción.

El último método y más importante es el execute, será el que realizará las acciones que sean necesarias.


  /**
   * {@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;
  }

En este caso cómo lo que queremos es generar el scaffolding para un QueueWorker le pasaremos los argumentos del comando al generator.

Ahora si nos pasamos a la clase encargada de generar el scaffolding PluginQueueWorkerGenerator veremos que está situada dentro de la carpeta src/Generator.

Generator directory QueueWorker Generator

Está clase deberá extender de Drupal\Console\Core\Generator\Generator e implementar Drupal\Console\Core\Generator\GeneratorInterface y pertenecer al paquete Drupal\Console\Generator.

<?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 {

Al igual que en la clase del commands si hemos definido algún servicio en el fichero console.services.yml deberemos definirlo en el constructor. En este caso hemos inyectado el 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;
  }

El único método que la interface nos obliga a implementar es la de generate, que es la que se encargará de hacer las acciones de generar el scaffolding.

En este caso se obtendrán las opciones del módulo donde queremos crear el scaffolding y el nombre de la clase para el QueueWorker.

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

Posteriormente vamos a definir cuál es el directorio donde está el template con $this->renderer->addSkeletonDir, el argumento que hay que pasarle es donde se va a encontrar el template twig, ya que la generación del scaffolding se realiza mediante un template twig.

El siguiente paso es generar el fichero con el método $this->renderFile, como primer argumento la ruta relativa a la carpeta templates.
El segundo argumento donde se va a situar y llamar el fichero una vez generado, con ayuda del servicio Extension Manager obtenemos la ruta del módulo donde se deben situar los plugins, con el método getPluginPath(<módulo>, <tipo_plugin>).
El último argumento serán las variables que queremos usar en el 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
    );
  }

Se ha creado el twig en la carpeta drupal_console_queue/console/templates/module/src/Plugin/QueueWorker/queue_worker.php

Template twig QueueWorker

 

Y ya lo tenemos listo. Eso sería todo lo necesario para crear un comando para Drupal Console que genere un plugin de QueueWorker. A partir de aquí puedes crear otro comando de Drupal Console que genere código simplemente adaptándolo al tipo de código que quieras generar.

Se ha querido contribuir este ejemplo al proyecto de Drupal Console; os dejamos los pull request para que podáis ver qué contienen. Por cierto, que ya fueron incluidas en Drupal Console :)

Pull request Drupal Console QueueWorker Generator.

Pull request traducción Drupal Console QueueWorker Generator.

Enlaces de interés.

Instalar Drupal Console.

Crear un comando custom con Drupal Console.

Repositorio del comando generado.