Drupal 8: Contenedor de Servicios e Inyección de Dependencias
19/06/2018 por keopx

Front.id

Uno de los saltos más significativos entre las versiones de Drupal 7 y Drupal 8 fue el salto de código procedural a la orientación de objetos (OOP).

Además Drupal 8 añade una serie de librerías y conceptos traídos de Symfony, entre los que destacaremos los Service Container y la Dependency Injection. También podríamos reseñar otros como el routing, pero si eso en otro artículo.

A continuación, vamos a ver cómo éstos se aplican en Drupal 8. Aunque parezcan nombres rimbombantes la verdad es que son conceptos bastante sencillos.

Veamos:

class Car {
 
  protected $engine;
 
  public function __construct() {
    $this->engine = new Engine();
  }
 
  /* ... */
 
}

Para crear una nueva instancia de la clase Car deberás hacer algo como esto:

$car = new Car();

Y ahora ya dispones de un objeto ($car) que tiene una propiedad $engine que a su vez controla a otro objeto. Resulta que para que el Car funcione debería tener un Engine. Para ello sería necesario extender la clase y sobrescribir su constructor para que cada Car tenga su Engine. ¿De verdad esto tiene sentido? La respuesta es NO.

Consideremos lo siguiente:

class Car {
 
  protected $engine;
 
  public function __construct(Engine $engine) {
    $this->engine = $engine;
  }
 
  /* ... */
 
}

Ahora la creación de una instancia de un objeto de esta clase, debería hacerse así:

$engine = new Engine();
$car = new Car($engine);

Bastante más limpio. Imaginemos que ahora tenemos que crear otro tipo de Engine, podemos hacerlo fácilmente sin preocuparnos demasiado en Car ya que supuestamente está creado para trabajar con cualquier Engine en su aplicación.

¡Esto es la inyección de dependencia!

La clase Car depende de un Engine para funcionar, así que se puede inyectar una ya creado en su constructor para que pueda funcionar. Así evitamos crear el engine en la clase Car, con lo que después no sería posible cambiar el engine.

La inyección en el constructor (Constructor injection) es lo más común, pero también encontrarás otros tipos, tales como "Property injection", donde un campo público de una clase lo inyecta directamente o "Setter injection", la inyección se realiza mediante un método setter.

Ejemplos:

Constructor Injection

namespace App\Mail;

// ...
class NewsletterManager
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Property Injection

// ...
class NewsletterManager
{
    public $mailer;

    // ...
}
# config/services.yaml
services:
     # ...

     app.newsletter_manager:
         class: App\Mail\NewsletterManager
         properties:
             mailer: '@mailer'

Setter Injection

// ...
class NewsletterManager
{
    private $mailer;

    public function setMailer(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    // ...
}

Entonces, ¿qué es eso de Contenedor de Servicios?¿Y que relación tiene con las Dependecy Injection?

Contenedor de Servicios y Dependecy Injection

Hasta ahora hemos visto un ejemplo sencillo donde únicamente le añadimos un Engine al Car, pero un coche tiene bastantes más características como son las ruedas, los frenos,...

Entonces, le pasamos más instancias de otras características (objetos).

El contenedor:

El contenedor crea una instancia de un objeto de esa clase, así como cada una de sus dependencias, a continuación, devuelve ese objeto de servicio. ¿Y cuál es la diferencia entre estos servicios (que como has visto son clases) que normalmente accedemos a través del contenedor y las otras clases PHP?

Este objeto especial, llamado contenedor de servicios, te permite estandarizar y centralizar la forma en que se construyen los objetos en tu aplicación. El contenedor facilita mucho tu trabajo, es muy rápido, y te obliga a usar una arquitectura que pone el énfasis en la creación de código reutilizable y desacoplado. Como todas las clases internas de Symfony2 usan el contenedor, en este capítulo aprenderás cómo extender, configurar y utilizar cualquier objeto en Symfony2. En gran medida, el contenedor de servicios es el responsable de la velocidad de ejecución y extensibilidad de Symfony2.

Podéis leerlo en detalle aquí

Un contenedor de servicios (o contenedor de inyección de dependencias) simplemente es un objeto PHP que gestiona la creación de instancias de los servicios (es decir, de los objetos).

Podéis leerlo en detalle aquí

Estáticamente, es muy simple, se utiliza el namespace global \Drupal para acceder a su método service() que devuelve el servicio con el nombre que se pasa a este. Muchos ejemplos hablan de servicios, pero la mayoría cubren sólo la forma estática de cargarlos.

$service = \Drupal::service('service_name');

Esta es un forma muy utilizada para cargar en nuestro .module una clase.

Podéis ver el uso aquí en el modulo workbench_moderation.

Servicios

Ahora, si estamos en una clase (por ejemplo, en Controller, Entity, Form, etc), debemos siempre inyectar el servicio como una dependencia de la clase.

Inyectar servicios en su propio servicio es muy fácil puesto que defines el servicio y todo lo que tiene que hacer es pasarle como un argumento al servicio que deseas inyectar. Veamos la siguiente definición de servicio:

workbench_moderation.services.yml

services:
...
  workbench_moderation.moderation_information:
    class: Drupal\workbench_moderation\ModerationInformation
    arguments: ['@entity_type.manager', '@current_user']
...

workbench_moderation/src/ModerationInformation.php

<?php

namespace Drupal\workbench_moderation;
...
class ModerationInformation implements ModerationInformationInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Creates a new ModerationInformation instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
    $this->entityTypeManager = $entity_type_manager;
    $this->currentUser = $current_user;
  }

Clases sin servicio

El ejemplo que más sencillo nos será de comprender es el de un controlador. Los controladores son usados para resolver direcciones dentro del site y el objetivo es que sean ligeros y se descargue la lógica sobre otras clases más pesadas.

Controller

Cuando se crea un objeto controlador (ControllerResolver::createController), el ClassResolver se utiliza para obtener una instancia de la definición de clase del controlador. El resolver es "consciente" del container y devuelve una instancia del controlador si el contenedor ya lo tiene. De lo contrario, instancia una nueva y devuelve esa nueva instancia.

Es aquí donde se genera nuestra inyección: si la clase que se resuelve implementa ContainerAwareInterface, la instancia se lleva a cabo utilizando el método estático create() en esa clase que recibe todo el contenedor. Y nuestra clase ControllerBase también implementa ContainerAwareInterface.

Veamos este ejemplo sencillo:

modules/block/src/Controller/BlockListController.php

/**
 * Defines a controller to list blocks.
 */
class BlockListController extends EntityListController {
 
  /**
   * The theme handler.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;
 
  /**
   * Constructs the BlockListController.
   *
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler.
   */
  public function __construct(ThemeHandlerInterface $theme_handler) {
    $this->themeHandler = $theme_handler;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('theme_handler')
    );
  }
}

Esto seria un Factory Pattern.

Otros ejemplos que podemos ver son los Form y los Plugins.

Los formularios son otro ejemplo de clases donde se pueden necesitar inyectar servicios. Normalmente, se pueden extender las clases de ConfigFormBase o FormBase que ya implementan ContainerInjectionInterface. En este caso, si anulas los métodos create() y __construct(), puedes inyectar lo que quieras. Si por el contrario no quieres extender estas clases lo único que has de hacer es implementar esta interfaz tu mismo y seguir los mismos pasos que hemos visto anteriormente con el controlador.

Como ejemplo, echemos un vistazo al HoneypotSettingsController que extiende el ConfigFormBase y veremos cómo inyecta los servicios en la parte superior de config.factory, que son las dependencias que son necesarias:

/**
 * Returns responses for Honeypot module routes.
 */
class HoneypotSettingsController extends ConfigFormBase {

 ...

  /**
   * Constructs a settings controller.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   The cache backend interface.
   */
  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, CacheBackendInterface $cache_backend) {
    parent::__construct($config_factory);
    $this->moduleHandler = $module_handler;
    $this->entityTypeManager = $entity_type_manager;
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
    $this->cache = $cache_backend;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('module_handler'),
      $container->get('entity_type.manager'),
      $container->get('entity_type.bundle.info'),
      $container->get('cache.default')
    );
  }

Como antes, el método cuando se instancia el create(), pasa al constructor del servicio las clases que son necesarias.

Básicamente más o menos es así cómo funciona la inyección en el constructor en Drupal 8.

Además, es importante comprender algunos subsistemas debido a que tienen algunas diferencias, pero el que es de crucial importancia de entender: los Plugins.

Plugins

El sistema de plugins es un componente muy importante de Drupal 8 puesto que proporciona mucha funcionalidad y flexibilidad. Vamos a ver cómo funciona la inyección de dependencia con las clases de plugins.

La diferencia más importante en cómo se maneja la inyección con los plugins con la implementación del interfaz de los plugins: ContainerFactoryPluginInterface. La razón es que los complementos no se resuelven, pero son administrados por un administrador general de plugins. Así que cuando este gestor necesita instanciar uno de sus plugins, lo hará utilizando una factory. Por lo general, este factory es el ContainerFactory (o una variación similar de la misma ContainerFactoryPluginInterface).

Si nos fijamos en ContainerFactory::createInstance(), vemos que aparte de pasarle el ContainerInterface al método usual, también se pasan las variables, $configuration, $plugin_id y $plugin_definition (que son las tres parámetros básicos que vienen con cada plugin).

Observamos que existe un plugin de tipo @Action que se define antes del class DeleteRedirect.

Como se puede ver, implementa el ContainerFactoryPluginInterface y el método create() que recibe esos tres parámetros adicionales. Éstos se pasan a su vez y en el mismo orden al constructor de clase. Además se le pasa dos container como servicios extra. Este es un ejemplo básico, pero en un ejemplo de uso general de como inyectar servicios en clases de plugins.

/**
 * Redirects to a redirect deletion form.
 *
 * @Action(
 *   id = "redirect_delete_action",
 *   label = @Translation("Delete redirect"),
 *   type = "redirect",
 *   confirm_form_route_name = "entity.redirect.multiple_delete_confirm"
 * )
 */
class DeleteRedirect extends ActionBase implements ContainerFactoryPluginInterface {

  /**
   * The tempstore object.
   *
   * @var \Drupal\user\SharedTempStore
   */
  protected $privateTempStore;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new DeleteRedirect object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
   *   The tempstore factory.
   * @param AccountInterface $current_user
   *   Current user.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
    $this->currentUser = $current_user;
    $this->privateTempStore = $temp_store_factory->get('redirect_multiple_delete_confirm');

    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('user.private_tempstore'),
      $container->get('current_user')
    );
  }

Documentación oficial de Drupal 8

Este artículo es bastante completo, pero siempre viene leer la documentación oficial Services and dependency injection in Drupal 8

Agregar nuevo comentario

El contenido de este campo se mantiene privado y no se mostrará públicamente.

HTML Restringido

  • Etiquetas HTML permitidas: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Las líneas y los párrafos se rompen automáticamente.
  • Las direcciones de las páginas web y las direcciones de correo electrónico se convierten en enlaces automáticamente.