· CMS

Services et configurations simples avec Drupal 8

Les services dans Drupal 8

Qu’est-ce qu’un service ?

Un service est une classe qui propose des fonctionnalités spécifiques et globales à toute l’application. L’exemple le plus couramment utilisé est un service permettant l’envoi de mail depuis n’importe quel endroit de l’application.

Une des principales caractéristiques du service est sa méthode d’instantiation par un Service Container. Celui-ci s’occupe de la création et l’initialisation de l’objet. Drupal a adopté le concept de Service Container depuis la version 8, basée sur le framework Symfony1.

Pour un développeur, il est conseillé d’utiliser le Service Container de Drupal pour respecter le découplage du système et la réutilisation des différentes briques logicielles.

Un service peut avoir besoin de paramètres ou de l’injection d’autres services2 qui sont alors définis dans son fichier YAML de déclaration. L’utilisation d’un service sous Drupal 8 est pratiquement la même que sous Symfony 2.

Le core de Drupal 8 expose lui-même de nombreux services3.

Ce qu’on faisait avant

Dans Drupal 7, nous aurions pu utiliser la fonction module_invoke pour pouvoir appeler une fonction d’un autre module par exemple :

module_invoke('MYMODULE', 'function_name', arg1, arg2,…)

L’utilisation de cette méthode crée un fort couplage entre deux modules.

Ce qu’on fait en Drupal 8

Avec l’injection de dépendance, il suffit de changer la classe utilisée dans le service pour changer le comportement de celui-ci qui sera alors transparent pour l’utilisateur du service.

Il est aussi possible de surcharger les services déja existants du core Drupal en utilisant un alias de service dans le sites/default/service.yml pointant vers notre service de remplacement.

Par exemple4 :

services :
  user.data :
    alias : monmodule.user.data

Comment déclarer un service dans votre module Drupal 8

Je vais prendre pour exemple un service de calcul de prix. La déclaration d’un service se fait à l’aide d’un fichier YAML. Le nom du fichier de service doit contenir le nom de mon module en premier.

Je vais alors créer un fichier price.services.yml (ici price est le nom du module). Ce service est encore simple et il ne prend pas d’arguments.

# price.services.yaml
services :
  price.calculator :
    class : Drupal\price\Service\PriceCalculator
    arguments : []

Je vais maintenant créer la classe PriceCalculator dans le dossier MonModule/src/Service. Ici, rien de bien compliqué, juste une classe simple avec un constructeur vide et une fonction de calcul qui prend en entrée une liste de produits et qui renvoie le prix hors taxe.

// PriceCalculator.php

namespace Drupal\price\Service ;

class PriceCalculator
{
    public function __construct(){}

    public function getFinalPriceHT($products)
    {
        $finalPrice = 0 ;
     foreach($products as $product){
            $finalPrice += $product->getPrice() ;
        }
        return $finalPrice ;
    }   
}

Utiliser un service dans votre module

Pour réutiliser le service il suffit de l’appeler en utilisant le Service Container de Drupal.

$priceCalculator  = \Drupal ::service(‘price.calculator') ;

Le Service Container va alors instantier l’objet, l’initialiser et le renvoyer. Il ne reste alors plus qu’à l’utiliser pour calculer le prix hors taxe.

$finalPrice = $priceCalculator->getFinalPriceHT($products) ;

De la même façon, on peut utiliser un service qui fait partie du core de Drupal (ici le service d’envoi de mails ) :

\Drupal ::service(‘plugin.manager.mail’)->mail(…)

Utiliser l’injection de dépendance dans un controleur

Pour éviter la redondance de l’appel à Drupal ::service, il est possible d’utiliser le container de Drupal. Pour cela, il suffit d’étendre la classe ControllerBase et de récuperer les services dans la méthode create du contrôleur avec son container. Voici un exemple d’injection de deux services :

// CalculController.php

class CalculController extends ControllerBase {

    protected $entity_query ;
    protected $priceCalculator ;

    public function __construct(QueryFactory $entity_query, PriceCalculator $calculator) {
        $this->entity_query = $entity_query ;
        $this->priceCalculator = $calculator ;
    }

    public static function create(ContainerInterface $container) {
        return new static(
            $container->get('entity.query'),
            $container->get('price.calculator')
        ) ;
    }
}

Cette méthode se rapproche de l’utilisation des services dans les contrôleurs du core de Drupal.

Les variables de configuration d’un module dans Drupal 8

Drupal 8 permet de rajouter des variables de configuration qui s’initialisent à l’activation du module. Une fois le module activé, nous pourrons faire des appels pour récupérer les variables de configuration du module.

Comment les déclarer ?

Pour déclarer les paramètres de configuration, il faut créer un fichier YAML dans le dossier MonModule/config/install de mon module. Le nom du fichier de configuration doit contenir le nom du module en premier. Je vais créer pour ce module de prix le fichier price.settings.yml. Dans celui-ci je vais mettre juste une variable qui, pour mon test, va représenter la TVA.

# price.calculator.yml

tva : 19.6

Comment les rendre administrables ?

Comme dit plus tôt, ce sont des variables. Il est donc possible de les rendre configurables par l’utilisateur. Pour cela, il suffit d’utiliser le setter de la méthode statique config() de Drupal 8.

\Drupal ::config("price.settings")->set("tva","7,7") ;

Il faut maintenant utiliser la méthode get pour récupérer la valeur de ma configuration :

\Drupal ::config("price.settings")->get("tva") ;

Il est aussi possible d’utiliser les formulaires de configuration de Drupal 85. Pour cela il faut créer une classe de formulaire qui étend la classe ConfigFormBase de Drupal 8.

Allier les configurations et les services

Maintenant qu’il y a la TVA dans la configuration du module, nous allons pouvoir l’utiliser dans le service. Je peux maintenant déclarer un paramètre dans mon fichier YAML et l’envoyer à mon service pour qu’il puisse l’utiliser. Pour cette tâche la déclaration de service devient dans mon yaml :

# price.service.yml

parameters :
  settings.name : price.settings
services :
  price.calculator :
    class : Drupal\price\Service\PriceCalculator
    arguments : [%settings.name%]

Dans ma classe, je vais pouvoir initialiser la variable tva avec la variable de configuration de mon module. Je peux utiliser tva pour créer une fonction getFinalPriceTtc() qui va permettre de calculer le prix total avec la TVA. Une méthode setTva(…) a été rajoutée pour mettre à jour la TVA. Ma classe devient alors :

// PriceCalculator.php

namespace Drupal\price\Service ;

class PriceCalculator
{
    private tva ;

    public function __construct($configName)
    {
        $config = \Drupal ::config($configName) ;
        $this->tva = $config->get("tva") ;
    }

    public function getFinalPriceHT($products)
    {
        $finalPrice = 0 ;
        foreach($products as $product){
            $finalPrice += $product->getPrice() ;
        }

        return $finalPrice ;
    }   

    public function getFinalPriceTTC($products)
    {
        $finalPrice = 0 ;

        foreach($products as $product){
            $finalPrice += $product->getPrice() ;
        }

        $finalPrice = $finalPrice + (($finalPrice*$this->tva)/100) ;

        return $finalPrice ;
    }   

    public function setTva($tva)
    {
        $this->tva = tva ;
   }    
}

  1. voir la documentation du Service Container de Symfony 2
  2. voir « Services and dependency injection in Drupal 8« , dans la documentation communautaire Drupal. 
  3. voir la documentation des services de Drupal 8
  4. exemple repris de « Overriding Drupal 8 Services », par Tim Millwood 
  5. la création d’un formulaire de configuration est bien expliquée dans « How to Create an Administration Form in Drupal 8 », de Ian Zugec 

2 commentaires

  1. Nicolas C

    Hello,
    tout d’abord merci pour cet article.
    Une question cependant: peut-on mélanger les genres, et avoir une classe développée sous Symfony 2.8, et s’en servir en tant que service dans Drupal 8 ?
    Et question complémentaire, si j’ai développé un bundle qui fait tout ce dont j’ai besoin dans une fonctionnalité de site D8, est ce que je peux implémenter mon bundle SF en service dans Drupal 8 ?

    M’est avis que ce doit être galère, mais ça m’intéresse fortement d’avoir des avis et retours sur tout cela !

    merci d’avance.

    N.

    1. Simon Caule

      Bonjour Nicolas,

      Je pense que tu vas surtout être limité par ton injection de dépendance car D8 n’étant pas un FullStack Symfony tu n’auras pas accès à certains services du Symfony et inversement par exemple dans mon code « \Drupal::config » ne marchera pas sur Symfony.

      Par contre une classe avec uniquement du code métier ne posera a priori aucun problème de réutilisation.

      Pour la seconde question si j’ai bien compris tu as développé un bundle SF que tu voudrais utiliser dans ton CMS en D8.

      Sauf erreur de ma part tu ne pourras pas utiliser directement ton bundle, Il va falloir que tu crées un module D8 et que tu copies tes services métier à l’intérieur.

      Si tu veux utiliser un bundle Symfony dans un cms directement tu peux regarder eZPublish qui est un FullStack Symfony.

Les commentaires sont désormais fermés.