Drupal 8 à votre service !

Un café sur un table

A l'instar des Plugins, Drupal 8 a introduit un nouveau concept, repris de Symfony2, dans son API : les Services. Ces derniers permettent de mettre à disposition des fonctionnalités assurant une et seule tache, comme par exemple envoyer un courrier électronique ou encore construire le fil d'ariane. La différence majeure des Services comparés aux Plugins est que les services ne disposent pas généralement (ou tout du moins directement) d'une interface graphique de configuration. Pour reprendre la définition concise :

Un service est un objet PHP, conçu dans le but d'atteindre un objectif spécifique, qui effectue une sorte de tâche globale.

Les services simples

Les services permettent de découpler des fonctionnalités réutilisables, de les rendre accessibles facilement grâce au Service container, et qui peuvent être altérées autant que de besoin. Le Service container est quant à lui un objet PHP conçu pour gérer les services et les instancier à la demande.

Les services peuvent être appelées de plusieurs façons différentes, au moyen du Service container, selon l'endroit où vous en avez besoin :

  • Depuis un code procédural

Vous pouvez utiliser la méthode globale \Drupal::service('service_name').

  • Depuis une classe PHP

Vous devez utiliser l'injection de dépendance pour accéder aux services. Il est possible, techniquement parlant, d'utiliser la méthode globale au sein d'une classe PHP, mais ceci est une mauvaise pratique car cette utilisation empêche alors votre code d'être testé unitairement. Bref, nous ne devons jamais voir un appel à \Drupal::service() au sein d'une classe PHP.

Les services sous Drupal 8 peuvent être utilisées de différentes façons : soit vous créez votre propre service, pour découpler des fonctionnalités unitaires et éviter de la duplication de code, soit vous pouvez (et devrez) utiliser et étendre les services fournis par le coeur de Drupal.

Les collecteurs de service

Drupal 8 dispose également d'un type spécial de service : un Service Collector. Ce type de service ne réalise pas de taches particulières en tant que tel, mais a pour vocation de collecter un ensemble de services qui auront souscrit au Service Collector. Un Service Collector est déclaré en tant que tel au moyen des propriétés des services, notamment via la propriété tags. Un Service Collector peut collecter un ou une infinité de services, qui tous auront souscrits à ce collecteur, qui pourra alors les instancier, les prioriser et les executer ou non selon les argurments fournies.

Ce système permet une grande souplesse quant aux différents comportements souhaités pour un seul et même service, mais qui aura toujours un seul et même objectif, et donc une tache spécifique à réaliser.

Pour illustrer le propros, la construction du fil d'ariane est assurée par le collecteur de service breadcrumb :

breadcrumb:
  class: Drupal\Core\Breadcrumb\BreadcrumbManager
  arguments: ['@module_handler']
  tags:
    - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }

Ce service est déclaré en tant que collecteur de services avec la propriété name et une valeur service_collector, les services souhaitant être collectés devront utililiser cette même propriété name des tags, mais avec la valeur breadcrumb_builder. Enfin la méthode addBuilder sera appliquée à chaque service collecté par la classe principale BreadcrumbManager.

A noter que le service module_handler est passé en argument à ce collecteur de service, et sera disponible au sein de la classe du service grâce à l'injection de dépendance.

Drupal fournit par défaut quelques services chargés de construire le fil d'ariane. Notamment pour les commentaires, les livres, les forums. Le module System fournit un constructeur par défaut chargé de calculer le fil d'ariane en fonction des différentes parties de l'url.

system.breadcrumb.default:
  class: Drupal\system\PathBasedBreadcrumbBuilder
  arguments: ['@router.request_context', '@access_manager', '@router', '@path_processor_manager', '@config.factory',  '@title_resolver', '@current_user', '@path.current']
  tags:
    - { name: breadcrumb_builder, priority: 0 }

Ce service sera ainsi collecté (avec une priorité de 0) par le Service Collector chargé de calculer le fil d'ariane, puis executé en fonction de sa propre logique. Si deux services collectés sont éligibles pour calculer le fil d'ariane, alors celui ayant la priorité la plus haute sera retenu.

Après cet aperçu général des services, découvrons plus en détail la création d'un service.

Création d'un service Drupal 8

Pour prendre un exemple simple, nous allons créer un service qui va nous pemettre de générer des contenus de façon programmatique.

Pour déclarer son propre service, il suffit de le déclarer dans votre module, intitulé par exemple flocon, au sein d'un simple fichier de configuration flocon.services.yml

services:
  flocon.manager:
    class: Drupal\flocon\FloconManager
    arguments: ['@entity_type.manager', '@transliteration']

Nous déclarons ici un nouveau service intitulé flocon.manager, lui attribuons une classe FloconManager et lui passons en arguments deux autres services, entity_type_manager qui va nous permettre d'accéder aux différentes entités de Drupal (Node, User, Block, File, etc.) et transliteration, un service utilitaire de transliteration.

Il ne nous reste plus qu'à créer notre fichier FloconManager.php dans le dossier src à la racine de notre module.

<?php

/**
 * @file
 * Contains \Drupal\flocon\FloconManager.
 */

namespace Drupal\flocon;

use Drupal\Component\Utility\Random;
use Drupal\Component\Transliteration\PhpTransliteration;
use Drupal\Core\Entity\EntityTypeManager;

/**
 * Provide utilities for creating content.
 */
class FloconManager {

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

  /**
   * A PHPTransliteration object.
   *
   * @var \Drupal\Component\Transliteration\PhpTransliteration
   */
  protected $transliteration;

  /**
   * Constructs a EntityCreateAccessCheck object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManager $entitytypemanager
   *   The node storage interface.
   * @param \Drupal\Component\Transliteration\PhpTransliteration $transliteration
   *   A PHPTransliteration object.
   */
  public function __construct(EntityTypeManager $entitytypemanager, PhpTransliteration $transliteration) {
    $this->entitytypemanager = $entitytypemanager;
    $this->transliteration = $transliteration;
  }

Les deux services passés en argument dans notre fichier de déclaration sont automatiquement instanciés par le service et sont passés au constructeur de la classe pour les rendre disponible pour un usage ultérieur.

Nota : A noter, que pour un plugin par exemple, il nous aurait fallu implémenter l'interface ContainerInjectionInterface (si notre classe n'étendait pas une classe implémentant déjà cette interface) et utiliser le service container avec la méthode create pour obtenir nos 2 services instanciés grâce à l'injection de dépendance.

Nous pouvons alors créer notre méthode qui va générer nos contenus.

/**
 * Create a node.
 *
 * @param string $type
 *   The bundle.
 * @param string $title
 *   The title.
 * @param string $langcode
 *   The langcode.
 * @param string $pattern
 *   The pattern to use for generate path alias.
 * @param string $uid
 *   The owner uid.
 * @param array $fields
 *   An array of fields to populate.
 * @param bool $promote
 *   Is the node is promoted to frontpage.
 * @param bool $status
 *   Is the node is published.
 *
 * @return \Drupal\node\Entity\Node $node
 *   The node object created.
 */
public function createNode($type, $title, $langcode, $pattern = '', $uid = '1', $fields = [], $promote = FALSE, $status = TRUE) {
  // Add trailing slash to the pattern and build the path.
  $pattern = empty($pattern) ? '' : $pattern . '/';
  $path = '/' . $pattern . $this->cleanAlias($title, $langcode);

  /** @var \Drupal\node\Entity\Node $node */
  $node = $this->entitytypemanager->getStorage('node')->create([
    'type' => $type,
    'title' => $title,
    'uid' => $uid,
    'status' => $status,
    'promote' => $promote,
    'langcode' => $langcode,
    'path' => $path,
  ]);

  if (empty($fields)) {
    $node->save();
  }
  else {
    foreach ($fields as $field_name => $value) {
      if ($node->hasField($field_name)) {
        $node->set($field_name, $value);
      }
    }
    $node->save();
  }
  return $node;
}

et quelques autres méthodes utilitaires pour compléter l'exemple.

/**
 * Helper function to return a clean alias.
 *
 * @param string $text
 *   Text to transliterate.
 * @param string $langcode
 *   The langcode.
 *
 * @return mixed
 *   the clean alias from $text
 */
protected function cleanAlias($text, $langcode = 'FR') {
  $text = $this->transliteration->transliterate($text, $langcode);
  return preg_replace('/\-+/', '-', strtolower(preg_replace('/[^a-zA-Z0-9_-]+/', '', str_replace(' ', '-', $text))));
}

/**
 * Return a random object.
 *
 * @return \Drupal\Component\Utility\Random
 *   Return a random object.
 */
public function getRandom() {
  return new Random();
}

Nous pourrons alors utiliser notre service pour générer simplement autant de contenus que nécessaire.

Depuis un code procédural, par exemple

/** @var \Drupal\flocon\FloconManager $flocon_manager */
$flocon_manager = \Drupal::service('flocon.manager');
$langcode = \Drupal::config('system.site')->get('langcode');

$fields = [
  'body' => [
    'value' => $flocon_manager->getRandom()->paragraphs(2),
    'format' => 'full_html',
  ],
  // More fields here.
];

$node = $flocon_manager->createNode('page', $flocon_manager->getRandom()->word(8), $langcode, 'informations', '1', $fields, TRUE);

Ou encore depuis une classe PHP (j'ai allégé la classe de ses commentaires, pour l'exemple)

/**
 * Custom class.
 */
class FloconTest implements ContainerInjectionInterface {
  /**
   * The Flocon Manager service.
   * @var \Drupal\flocon\FloconManager
   */
  protected $floconmanager;

  public function __construct(FloconManager $floconmanager) {
    $this->floconmanager = $floconmanager;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('flocon.manager')
    );
  }

  protected function generateNode($type, $title, $langcode, $pattern, $uid = '1', $fields = [], $promote = FALSE, $status = TRUE) {
    return $this->floconmanager->createNode($type, $title, $langcode, $pattern, $uid, $fields, $promote, $status);
  }
  
}

Drupal 8 : un service au top !

Ces quelques exemples vous auront convaincu, je l'espère, d'user et d'abuser des services. Et pour savoir quand il est nécessaire d'y recourir, c'est simple : au premier copier / coller détecté dans votre code. Et enfin, et non des moindres, un service conçu pour un besoin premier peut être réutilisé à volonté et partagé.

Pour conclure, un petite astuce. Si vous utilisez l'IDE PHPStorm, l'extension Drupal Symfony2 bridge vous offrira (entre autres) une autocomplétion lors de l'appel au Service Container. Sans parler de tous les autres apports (autocomplétion des routes, des variables Twig, etc). Comme pour drush, l'essayer, c'est l'adopter.

Vous hésitez à vous lancer ? Contactez moi pour discuter de votre projet Drupal 8.

Ajouter un commentaire