Améliorer le système d'injection de services des formulaires Drupal 8

Contribué par Sylvain Lavielle le 01/07/2019

Drupal comme la plupart des socles applicatifs PHP modernes repose beaucoup sur les services et l'injection de services.

Il existe globalement 2 méthodes pour injecter des services par le constructeur des classes (injection par contructeur) :

C'est à ce 2eme cas que nous allons nous intéresser dans cet article.

De quoi s'agit il ?

Voici comment se passe une injection de services classique dans un formulaire.

namespace Drupal\newsletter\Form;
 
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxy;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
class TestForm extends FormBase {
 
  protected $currentUser;
 
  protected $entityTypeManager;
 
  public function __construct(
    AccountProxy $current_user, 
    EntityTypeManager $entity_type_manager
  ) {
    $this->currentUser = $current_user;
    $this->entityTypeManager = $entity_type_manager;
  }
 
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('current_user'), 
      $container->get('entity_type.manager'),
    );
  }
  ...

Le principe est assez simple : Les classes issues d'une factory ne sont pas créées directement. La factory appelle une méthode create qui doit construire et renvoyer l'instance de l'objet issue de la classe.

Le conteneur de service est passé en paramètre de la méthode create permettant de récupérer des services et de les passer en argument au constructeur qui à son tour s'en servira pour associer ces instances de services à des propriétés de la classe ce qui permettra de les utiliser partout dans la classe.

Maintenant, que se passe-t-il si on souhaite étendre les classes formulaires ?

Imaginons le cas suivant :

  • Je souhaite développer un ensemble de formulaires fonctionnellement assez proches mais dont chacun à des spécificités
  • Ces formulaires utilisent tous un même sous-ensemble de services mais certains d'entre eux font appel à des services supplémentaires.

Pour répondre à ce besoin, la solution que nous allons retenir est donc d'étendre une classe abstraite (que nous appellerons TestFormAbstract) de la classe Drupal FormBase de façon à y ajouter les comportements communs, puis nous allons étendre à nouveau cette classe abstraite pour chacun de nos formulaires finaux (Test1Form, Test2Form, etc).

Mais comment cela va t-il se passer avec la méthode create ? Cette méthode ne peut pas être étendue puisqu'elle appelle le constructeur de la classe à la fin. On peut la redéfinir au niveau de chaque classe de formulaire finale mais cela oblige à redéfinir intégralement l'ensemble des services pour chacune de ces classes, ce qui est très dommage et n'est pas très compatible avec le système d'héritage que nous souhaitons mettre en place.

La solution

Voici la solution proposée :

Dans notre classe abstraite TestFormAbstract, nous allons créer une méthode nommée getServices qui va dans un premier temps renvoyer un tableau contenant les instances de services partagées par l'ensemble de nos formulaires finaux. Cette méthode pourra ensuite être étendue par chacune des classes qui étendent TestFormAbstract (Test1Form, Test2Form, etc) de façon à ajouter leur propre service aux services déjà déclarés par la classe abstraite.

Sur notre classe abstraite TestFormAbstract nous allons également définir la fonction create comme fonction "final" et faire en sorte qu'elle récupère les services auprès de getServices pour les fournir au constructeur de la classe.

TestFormAbstract.php

namespace Drupal\newsletter\Form;
 
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Session\AccountProxy;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
Abstract class TestFormAbstract extends FormBase {
 
  protected $currentUser;
 
  protected $entityTypeManager;
 
  public function __construct(
    AccountProxy $current_user,
    EntityTypeManager $entity_type_manager
  ) {
    $this->currentUser = $current_user;
    $this->entityTypeManager = $entity_type_manager;
  }
 
  final public static function create(ContainerInterface $container) {
    return new static(...static::getServices($container));
  }
 
  protected static function getServices(ContainerInterface $container) {
    return [
      $container->get('current_user'),
      $container->get('entity_type.manager'),
    ];
  }
}

Ainsi nous obtiendrons un ensemble bien plus extensible puisque nous n'utiliserons plus la méthode create dans nos formulaires finaux pour définir nos services mais la méthode getService qui peut être étendue à volonté.

Test1Form.php

namespace Drupal\newsletter\Form;
 
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxy;
use Egulias\EmailValidator\EmailValidatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
class Test1Form extends TestFormAbstract {
 
  protected $emailValidator;
 
  public function __construct(
    AccountProxy $current_user,
    EntityTypeManager $entity_type_manager,
    EmailValidatorInterface $email_validator
  ) {
    parent::__construct($current_user, $entity_type_manager);
    $this->emailValidator = $email_validator;
  }
 
  protected static function getServices(ContainerInterface $container) {
    $services = parent::getServices($container);
    $services[] = $container->get('email.validator');
    return $services;
  }
 
  ...

Au passage, on peut noter l'emploi d'une syntaxe un peu particulière dans l'abstract :

    return new static(...static::getServices($container));
  • static fait référence à la classe courante et new static appelle par conséquent le constructeur de la classe
  • ...static::getServices($container) :
    • static::getServices($container) appelle la fonction static getServices.
    • les ... situés devant permettent de passer les paramètres à une méthode en utilisant un tableau plutôt que de les passer de façon classique argument par argument. C'était une nouveauté de PHP 5.6 (voir Variable-length argument lists ¶)