A la hora de solucionar un problema de desarrollo tenemos dos caminos, uno corto y uno largo. ¿Cuál nos interesa? A priori, cualquiera diría que el corto. Y, si bien es cierto que en algunos casos sería así, la verdad es que en la mayoría de los casos lo que nos interesa es el largo.

No te dejes engañar por la situación; eso de “es solo para solucionar mi problema” luego se convierte en “qué bien funciona esto que hiciste, ¿podemos añadirle no sé qué?”. Cuando se te dé el caso, sabrás que el camino corto se habrá convertido en dos veces el camino largo.

Entonces, ¿qué soluciones tenemos para el camino largo? Y la respuesta es:

  • Una jerarquía de clases bien diseñada, flexible, con la especialización en su lugar y fruto de un buen montón de horas en la fase de diseño.
  • Un esquema de configuración con una plétora de opciones que prevea tantas situaciones como sea posible y, como la anterior, sea fruto de una buena fase de diseño.
  • Un modelo ampliable, simple, conciso, asumiendo que siempre faltará algo por hacer, añadir o contemplar.

Curiosamente, aplicando el modelo ampliable, el camino largo del primer proyecto se va acortando en el segundo y en los consecutivos. Esto que he llamado modelo ampliable tiene el nombre técnico de “Plug-in Pattern”. Haciendo el símil con los enchufes de corriente de las casas. Éstas están vacías y casi sin servicios cuando entramos a vivir, pero luego las llenamos con decenas de cosas enchufadas que mejoran sus funciones.

Hay muchísimos casos de éxito con este patrón. Sin salirnos de nuestro campo, tenemos por ejemplo, el PC de IBM y los sistemas operativos modernos; si nos salimos fuera, los coches, que tienen miles de piezas comunes entre distintos modelos, incluso marcas, y a los que podemos ir añadiendo extras, plugins, con el tiempo.

 

 

¿CÓMO APLICAR UN SISTEMA DE PLUGINS?

Para aplicar con éxito un sistema de plugins necesitamos resolver algunas cuestiones:

  • ¿Cómo sé que hay un plugin disponible?
  • De los plugins disponibles, ¿cuáles tengo que aplicar y dónde?
  • Si tengo que aplicar un plugin, ¿cómo lo hago?

Siguiendo el símil de la casa, pongamos que queremos encontrar una solución a una situación de frío en el comedor. Las respuestas a las preguntas anteriores serían:

  • ¿Cómo sé que hay un plugin disponible? Me voy a un centro comercial o ecommerce y miro su oferta.
  • De los plugins disponibles, ¿cuáles tengo que aplicar y dónde? De las estufas que tengan miro una que me valga para aplicarla en el comedor, el donde.
  • ¿Cómo lo hago? Me compro una, y la enchufo en el comedor.

Trasladándonos al desarrollo de aplicaciones, veamos el ejemplo de cómo solucionar la limpieza de una cadena de texto de cosas indeseadas:

  • ¿Cómo sé que hay un plugin disponible? Nuestra aplicación debe tener un sitio donde consultar los plugins instalados y su tipo. Como más adelante veremos.
  • De los plugins disponibles, ¿cuáles tengo que aplicar y dónde? Escogemos aquellos que estén activos y que sean para la limpieza de textos, y los aplicamos al texto que estemos tratando en ese momento.
  • ¿Cómo lo hago? Recorro la lista de plugins resultante de la pregunta anterior y les voy diciendo uno a uno que limpien el texto.

El concepto es simple, ¿verdad? Pues quedémonos con eso, puesto que realmente lo es. Y, como veremos más adelante, en Drupal 8 es más de lo mismo.

 

 

PATRONES DE DISEÑO

Antes de seguir, aclarar que aunque vamos a hablar de plugins en Drupal 8 como algo concreto, realmente es una forma de trabajar abstracta. Es lo que se conoce como un patrón de diseño. Además se necesita comprender otros patrones complementarios para entender todo en su conjunto. Por eso, empecemos explicando qué es un patrón de diseño.

Un patrón de diseño es una solución repetible para un problema recurrente. No pretende dar una solución completa, traducible a código, solo es una plantilla sobre cómo resolver el problema que pueda ser usada en distintas situaciones.

Para el caso concreto que nos reúne aquí creo que debemos ver los patrones que vamos a necesitar, por ser los usados en el sistema de plugins de Drupal 8.

 

PATRÓN PLUG-IN (enchufable)

Este patrón de diseño extiende la funcionalidad de una clase existente para usarla en situaciones más concretas que para la que fue diseñada inicialmente, pero sin alterar la funcionalidad original.

En contraste, tenemos la herencia típica de la orientación a objetos, que modifica o sobrescribe la funcionalidad original, o la configuración, donde las modificaciones están limitadas a las opciones de configuración disponibles, y que hayan sido previstas durante la fase de diseño inicial.

 

PATRÓN DEPENDENCY INJECTION (inyección de dependencias)

Permite integrar instancias en una clase en lugar de confiar en la propia clase a la hora de crear esos objetos. De ahí su segundo nombre: inversión de control de contenedores (Inversion of Control Containers). Es muy útil a la hora de desacoplar los detalles de implementación o desarrollo de tu aplicación, mejorando la reusabilidad del código implicado.

La responsabilidad de manejar las interdependencias de código, incluidas la instanciación de objetos y el enlazado, queda excluida de la clase en sí y es transferida a otro ente, indeterminado, que se determinará cuando sea necesario.

Esto permite compartir contenedores (containers) en multitud de situaciones, cosa que no se puede hacer con el patrón de factorías (factory pattern), por ejemplo.

 

PATRÓN DE FACTORÍAS (fabricadores)

Este patrón usa una entidad abstracta para crear otra. Sí, a mí me genera tantas preguntas como a ti.

En otras palabras, define una interfaz para la creación de objetos, pero deja a las subclases decidir qué clase instanciar. Esto hace al operador ‘new’ totalmente inútil.

El patrón define cómo crear instancias, pero la responsabilidad de creación se dispersa dentro de la jerarquía. Es decir, no se sabe de principio dónde se creará la instancia, lo que lo dota de una gran flexibilidad haciendo un uso extensivo del polimorfismo.

 

 

PLUG-IN API

Drupal contiene muchos plugins diferentes, de diferentes tipos. Este sistema provee un set de guías y componentes de código reutilizable que permite a los desarrolladores exponer componentes extensibles dentro de su código y soporte para manejar estos componentes a través de la interfaz, cuando es necesario.

Los plugins son definidos por los módulos. Estos pueden ofrecer plugins de distintos tipos y módulos diferentes puede disponer de sus propios plugins de un tipo en particular.

 

SERVICIO DE PLUG-INS / PLUG-IN MANAGER

Es la clase controladora central que define cómo se descubren los plugins y cómo instanciarlos. Ésta se llama directamente en cualquier módulo que desee invocar un tipo de plugin.

Aunque existen otros generadores, usaremos el de descubrimiento por anotaciones por ser el más extendido.

A continuación, vamos a ver un ejemplo de crear el esqueleto de un plugin manager con la consola de Drupal: generate:plugin:type:annotation

    1. Generamos un módulo donde definir el tipo de plugin:

      drupal generate:module \
      module="SDOS Plugin API Example"  \
      machine-name="sdos_papi_example"  \
      module-path="modules/custom"  \
      description="Un ejemplo de modulo ampliable."  \
      core="8.x"  \
      package="S-DOS"  \
      module-file  \
      composer  \
      test  \
      twigtemplate
      

      Nota: el generador añade unas comillas de más en el archivo info.yml. Esto genera un error irrecuperable en Drupal, por lo que siempre conviene revisar cada archivo generado.

    2. Se generarán los siguientes archivos:

      /modules/custom/sdos_papi_example/sdos_papi_example.info.yml
      /modules/custom/sdos_papi_example/sdos_papi_example.module
      /modules/custom/sdos_papi_example/composer.json
      /modules/custom/sdos_papi_example/tests/src/Functional/LoadTest.php
      /modules/custom/sdos_papi_example/sdos_papi_example.module
      /modules/custom/sdos_papi_example/templates/sdos-papi-example.html.twig
      
    3. Generamos el plugin manager (a través de la creación de un tipo de plugin, ya que la definición del tipo se hace en el propio manager):

      drupal generate:plugin:type:annotation \
      module="sdos_papi_example"  \
      class="PapiExamplePlugin"  \
      machine-name="papi_example_plugin"  \
      label="Plugin API example plugin"
    4. Se generarán los siguientes archivos:

      modules/custom/sdos_papi_example/src/Annotation/PapiExamplePlugin.php
      modules/custom/sdos_papi_example/src/Plugin/PapiExamplePluginBase.php
      modules/custom/sdos_papi_example/src/Plugin/PapiExamplePluginInterface.php
      modules/custom/sdos_papi_example/src/Plugin/PapiExamplePluginManager.php
      modules/custom/sdos_papi_example/sdos_papi_example.services.yml
      
    5. Generamos también una ruta donde ver los ejemplos:

      drupal generate:controller \
      module="sdos_papi_example"  \
      class="PapiDemoController"  \
      routes='"title":"PapiDemo", "name":"sdos_papi_example.papi_demo", "method":"papiDemo", "path":"/sdos_papi_example/papiDemo/{name}"'  \
      test
      
    6. Esto genera los siguientes archivos:

      modules/custom/sdos_papi_example/src/Controller/PapiDemoController.php
      modules/custom/sdos_papi_example/sdos_papi_example.routing.yml
    7. Modificamos el método papiDemo del archivo sdos_papi_example/src/Controller/PapiDemoController.php, generado en el paso anterior, como sigue:

      <?php
       
      namespace Drupal\sdos_papi_example\Controller;
       
      use Drupal\Core\Controller\ControllerBase;
       
      /**
       * Class PapiDemoController.
       */
      class PapiDemoController extends ControllerBase {
        /**
         * Papidemo.
         *
         * @return array
         *   Return Hello string.
         */
        public function papiDemo($name) {
      	// Default message.
      	$msg = $this->t('This is the @name page.', ['@name' => $name]);
      	$renderArray = [
        	'#type' => 'markup',
        	'#markup' => $msg,
        	'#prefix' => '<p>',
        	'#suffix' => '</p>',
      	];
      	// Page render array.
      	return $renderArray;
        }
      }
    8. Una vez generado, y modificado, activamos nuestro módulo con Drush:

      drush en sdos_papi_example

 

Y al visitar la URL http://localhost/sdos_papi_example/papiDemo/Pedro veremos algo así:

Plugins Drupal - Salida Módulo sin Plugins - SDOS

 

 

DESCUBRIMIENTO DE PLUGINS

Una de las principales responsabilidades del plugin manager es descubrir cualquier implementación del tipo de plugin (plugin type) que maneja. Para cumplir con esta responsabilidad dispone de varias opciones: Anotaciones, Hooks, YAML ó Statics.

Las anotaciones es la opción más extendida. Por eso, es la que utilizaremos para el ejemplo.

    1. Para continuar vamos a generar un nuevo módulo donde hacer uso del nuevo tipo de plugin implementando un plugin nuevo:

      drupal generate:module \
      module="SDOS Plugin Example"  \
      machine-name="sdos_plugin_example"  \
      module-path="modules/custom"  \
      description="Un ejemplo de  plugin para nuestro modulo ampliable."  \
      core="8.x"  \
      package="S-DOS"  \
      module-file  \
      composer  \
      test  \
      twigtemplate
    2. Esto genera los siguientes archivos:

      /modules/custom/sdos_plugin_example/sdos_plugin_example.info.yml
      /modules/custom/sdos_plugin_example/sdos_plugin_example.module
      /modules/custom/sdos_plugin_example/composer.json
      /modules/custom/sdos_plugin_example/tests/src/Functional/LoadTest.php
      /modules/custom/sdos_plugin_example/sdos_plugin_example.module
      /modules/custom/sdos_plugin_example/templates/sdos-plugin-example.html.twig
    3. Y ahora generamos un plugin para nuestro modelo:

      drupal generate:plugin:skeleton \
      module="sdos_plugin_example"  \
      plugin-id="papi_example_plugin"  \
      class="GlassesPapiExamplePlugin"
    4. Esto nos genera:

      modules/custom/sdos_plugin_example/src/Plugin/PapiExamplePlugin/GlassesPapiExamplePlugin.php
    5. El código generado, ya modificado, quedaría así:

      <?php
       
      namespace Drupal\sdos_plugin_example\Plugin\PapiExamplePlugin;
       
      use Drupal\sdos_papi_example\Plugin\PapiExamplePluginInterface;
       
      /**
       * @PapiExamplePlugin(
       *  id = "papi_example_plugin",
       *  label = @Translation("Add glasses."),
       * )
       */
      class GlassesPapiExamplePlugin implements PapiExamplePluginInterface {
        /**
         * The plugin ID of the mapper.
         *
         * @var string
         */
        protected $pluginId;
        /**
         * The name.
         *
         * @var string
         */
        protected $name;
        /**
         * The renderable array.
         *
         * @var array
         */
        protected $renderable;
        /**
         * Constructs the glasses plugin.
         *
         * @param array $parameters
         *   The instance parameters.
         * @param string $plugin_id
         *   The ID of the plugin to use by default.
         */
        public function __construct($parameters, $plugin_id) {
      	$this->pluginId = $plugin_id . '_' . time();
      	$this->name = $parameters['name'];
      	$this->renderable = $parameters['render'];
        }
        /**
         * {@inheritdoc}
         */
        public function build() {
      	$build = ['first_element' => $this->renderable];
      	// Implement your logic.
      	$build['glasses_element'] = [
        	'#type' => 'markup',
        	'#markup' => t('@name has glasses.', ['@name' => $this->name]),
        	'#prefix' => '<p>',
        	'#suffix' => '</p>',
      	];
      	return $build;
        }
        /**
         * {@inheritdoc}
         */
        public function getPluginId() {
      	// Gets the plugin_id of the plugin instance.
      	return $this->pluginId;
        }
        /**
         * {@inheritdoc}
         */
        public function getPluginDefinition() {
      	// Gets the definition of the plugin implementation.
        }
      }

      Nota: Lo importante aquí es el constructor, __construct, que nos da los parámetros de contexto, y el método build que nos permite hacer el trabajo.

    6. Una vez modificado, activamos el módulo con:

      drush en sdos_plugin_example
    7. Ahora nos falta modificar el módulo inicial para que aplique los plugins a la respuesta original. Para ello, en nuestro controlador buscaremos los plugins existentes y los ejecutaremos para que hagan sus cambios.
    8. El código quedará como sigue para /sdos_papi_example/src/Controller/PapiDemoController.php:

      <?php
      namespace Drupal\sdos_papi_example\Controller;
      use Drupal\Core\Controller\ControllerBase;
      /**
       * Class PapiDemoController.
       */
      class PapiDemoController extends ControllerBase {
        /**
         * Papidemo.
         *
         * @return array
         *   Return Hello string.
         */
        public function papiDemo($name) {
      	// Default message.
      	$msg = $this->t('This is the @name page.', ['@name' => $name]);
      	$renderArray = [
        	'#type' => 'markup',
        	'#markup' => $msg,
        	'#prefix' => '<p>',
        	'#suffix' => '</p>',
      	];
      	// Invoque the plugin class.
      	$type = \Drupal::service('plugin.manager.papi_example_plugin');
      	// Get a list of plugins available.
      	$plugin_definitions = $type->getDefinitions();
      	// Call each plugin to get the magic.
      	foreach ($plugin_definitions as $plugin_definition) {
        	$plugin = $type->createInstance(
          	$plugin_definition['id'],
          	[
            	'render' => $renderArray,
            	'name' => $name,
          	]
        	);
        	$renderArray = $plugin->build();
      	}
      	// Page render array.
      	return $renderArray;
        }
      }
    9. Ahora nuestro método de controlador papiDemo elabora la respuesta inicial, busca los plugins disponibles y hace pasar la respuesta por cada uno de ellos para que hagan su trabajo.

Al cargar la URL que definimos al principio, http://localhost/sdos_papi_example/papiDemo/Pedro, veremos algo como esto:

Plugins Drupal - Salida Módulo con Plugins - SDOS

 

 

MÁS SOBRE PLUGINS

En este post sólo hemos visto lo más básico del sistema de plugins de Drupal, que no es poco. En el tintero se nos quedan muchos más conceptos que quizás veamos otro día:

  • Discovery decorators
  • Plug-in Factories
  • Default Factory
  • Container Factory
  • Reflection Factory
  • Factorías personalizadas
  • Plug-in mappers