Utilisation de la promotion des propriétés du constructeur PHP dans les modules personnalisés Drupal
PHP 8 a introduit la promotion des propriétés du constructeur, une fonctionnalité qui simplifie la définition et l’assignation des propriétés de classe en vous permettant de déclarer et d’initialiser les propriétés directement dans la signature du constructeur. Ce tutoriel montre comment utiliser la promotion des propriétés du constructeur dans les modules personnalisés Drupal (qui requièrent PHP 8.0+), notamment pour simplifier l’injection de dépendances dans vos services et contrôleurs. Nous comparerons le modèle traditionnel de Drupal (utilisé en PHP 7 et dans les premières versions de Drupal 9) avec l’approche moderne de PHP 8+, en fournissant des exemples complets de code pour les deux. À la fin, vous verrez comment cette syntaxe moderne réduit le code répétitif, rend le code plus clair et s’aligne sur les meilleures pratiques actuelles.
Drupal 10 (qui nécessite PHP 8.1+) a commencé à adopter ces fonctionnalités PHP modernes dans le cœur, donc les développeurs de modules personnalisés sont encouragés à en faire de même. Commençons par revoir le modèle traditionnel d’injection de dépendances dans Drupal, puis refactorons-le en utilisant la promotion des propriétés du constructeur.
Injection de dépendances traditionnelle dans Drupal (avant PHP 8)
Dans les services et contrôleurs Drupal, le modèle traditionnel d’injection de dépendances comprend trois étapes :
-
Déclarer chaque dépendance comme une propriété de classe (généralement
protected
) avec un docblock approprié. -
Indiquer le type de chaque dépendance dans les paramètres du constructeur, puis les assigner aux propriétés de classe à l’intérieur du constructeur.
-
Pour les contrôleurs (et certaines classes de plugin), implémenter une méthode statique
create(ContainerInterface $container)
pour récupérer les services depuis le conteneur de services de Drupal et instancier la classe.
Cela génère beaucoup de code répétitif. Par exemple, considérons un service personnalisé simple qui a besoin de la fabrique de configuration et d’une fabrique de logger. Traditionnellement, vous écririez :
Exemple de classe de service traditionnelle
<?php
namespace Drupal\example;
/**
* Service exemple qui journalise le nom du site.
*/
class ExampleService {
/**
* Le service de fabrique de configuration.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Le service de fabrique de canal de journalisation.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Construit un objet ExampleService.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* La fabrique de configuration.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* La fabrique de canal de journalisation.
*/
public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
// Stocker les services injectés.
$this->configFactory = $config_factory;
$this->loggerFactory = $logger_factory;
}
/**
* Journalise le nom du site comme action exemple.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Nom du site : ' . $site_name);
}
}
Dans l’exemple ci-dessus, nous déclarons deux propriétés $configFactory
et $loggerFactory
et les assignons dans le constructeur. Le service correspondant doit aussi être défini dans le fichier services YAML du module (avec les services requis en arguments), par exemple :
# example.services.yml
services:
example.example_service:
class: Drupal\example\ExampleService
arguments:
- '@config.factory'
- '@logger.factory'
Quand Drupal instancie ce service, il passe les arguments configurés au constructeur dans l’ordre listé.
Exemple de classe de contrôleur traditionnelle
Les contrôleurs Drupal peuvent aussi utiliser l’injection de dépendances. Typiquement, une classe de contrôleur étend ControllerBase
(pour les méthodes utilitaires) et implémente l’injection du conteneur Drupal en définissant une méthode create()
. La méthode create()
est une fabrique qui récupère les services du conteneur et appelle le constructeur. Par exemple :
<?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Contrôleur pour les routes Example.
*/
class ExampleController extends ControllerBase {
/**
* Le service de gestionnaire de types d'entité.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Le service de traduction de chaînes.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* Construit un objet ExampleController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Le gestionnaire de types d'entité.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* Le service de traduction de chaînes.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Récupérer les services nécessaires depuis le conteneur et les passer au constructeur.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Construit une réponse de page simple.
*/
public function build(): array {
// Exemple d’utilisation des services injectés.
$node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
return [
'#markup' => $this->t('Il y a @count contenus sur le site.', ['@count' => $node_count]),
];
}
}
Utilisation de la promotion des propriétés du constructeur (PHP 8+) dans Drupal
La promotion des propriétés du constructeur simplifie le modèle précédent en vous permettant de déclarer et d’assigner les propriétés en une seule étape, directement dans la signature du constructeur. En PHP 8, vous pouvez préfixer les paramètres du constructeur avec une visibilité (et d’autres modificateurs comme readonly
), et PHP crée et assigne automatiquement ces propriétés pour vous. Cela signifie que vous n’avez plus besoin de déclarer séparément la propriété ni d’écrire l’assignation dans le constructeur — PHP s’en charge.
Important, il s’agit d’un sucre syntaxique. Cela ne change pas le fonctionnement de l’injection de dépendances de Drupal ; cela réduit simplement le code que vous écrivez. Vous enregistrez toujours les services dans YAML (ou laissez Drupal les autowirer), et pour les contrôleurs vous utilisez toujours la méthode fabrique create()
(sauf si vous enregistrez le contrôleur comme service). La différence se situe uniquement dans la façon d’écrire le code de la classe. Le résultat est une réduction significative du code répétitif, comme l’a démontré un ticket dans le cœur de Drupal où des dizaines de lignes de déclarations et d’assignations ont été réduites à quelques lignes dans le constructeur.
Refactorisons nos exemples en utilisant la promotion des propriétés du constructeur.
Classe de service avec promotion des propriétés du constructeur
Voici la classe ExampleService
réécrite avec la syntaxe des propriétés promues de PHP 8 :
<?php
namespace Drupal\example;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Service exemple qui journalise le nom du site (utilisant la promotion des propriétés).
*/
class ExampleService {
/**
* Construit un objet ExampleService avec les services injectés.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* La fabrique de configuration.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* La fabrique de canal de journalisation.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected LoggerChannelFactoryInterface $loggerFactory
) {
// Aucun corps nécessaire ; les propriétés sont automatiquement assignées.
}
/**
* Journalise le nom du site comme action exemple.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Nom du site : ' . $site_name);
}
}
Classe de contrôleur avec promotion des propriétés du constructeur
Considérons maintenant la classe ExampleController
refactorisée pour utiliser les propriétés promues :
<?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Contrôleur pour les routes Example (utilisant la promotion des propriétés).
*/
final class ExampleController extends ControllerBase {
/**
* Construit un ExampleController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Le gestionnaire de types d'entité.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* Le service de traduction de chaînes.
*/
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
private TranslationInterface $stringTranslation
) {
// Pas besoin d’assignations ; les propriétés sont automatiquement définies.
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Passer les services du conteneur au constructeur.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Construit une réponse de page simple.
*/
public function build(): array {
$node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
return [
'#markup' => $this->t('Il y a @count contenus sur le site.', ['@count' => $node_count]),
];
}
}
Avantages de la promotion des propriétés du constructeur
L’utilisation de la promotion des propriétés du constructeur dans vos classes Drupal offre plusieurs avantages :
-
Réduction du code répétitif : Vous écrivez beaucoup moins de code. Il n’est plus nécessaire de déclarer manuellement les propriétés et de les assigner, ce qui élimine de nombreuses lignes surtout lorsque votre classe a plusieurs dépendances. Cela rend vos modules plus propres et plus faciles à maintenir.
-
Code plus clair et concis : Les dépendances de la classe sont toutes visibles en un seul endroit – la signature du constructeur – plutôt que dispersées entre les déclarations de propriétés et le corps du constructeur. Cela améliore la lisibilité et rend immédiatement clair les services requis par la classe.
-
Moins de commentaires de documentation nécessaires : Parce que les propriétés sont typées dans le constructeur, vous pouvez omettre les annotations redondantes
@var
et@param
pour ces propriétés et paramètres (à condition que leur usage soit évident via les noms). Le code est largement auto-documenté. Vous pouvez toujours documenter ce qui n’est pas évident, mais il y a moins de répétition. -
Syntaxe PHP moderne : Adopter la promotion des propriétés signifie que votre code reste à jour avec les pratiques modernes de PHP. Le cœur de Drupal 10+ a commencé à utiliser cette syntaxe pour le nouveau code, donc l’utiliser dans vos modules personnalisés rend votre code plus cohérent avec les exemples du cœur et de la communauté. Cela prépare aussi votre base de code aux améliorations futures (par exemple, PHP 8.1+ permet l’usage du mot-clé
readonly
sur les propriétés promues pour des dépendances véritablement immuables).
Les performances et fonctionnalités restent identiques à l’injection traditionnelle – la promotion des propriétés est purement une commodité syntaxique. Vous obtenez toujours des propriétés pleinement typées que vous pouvez utiliser dans toute votre classe (par exemple, $this->entityTypeManager
dans l’exemple du contrôleur). En interne, le résultat est équivalent au code plus long ; c’est juste obtenu avec moins d’effort.
Conclusion
La promotion des propriétés du constructeur est une fonctionnalité simple mais puissante de PHP 8 que les développeurs Drupal peuvent exploiter pour simplifier le développement de modules personnalisés. En éliminant le code répétitif, elle vous permet de vous concentrer sur ce que fait réellement votre classe plutôt que sur la connexion des services. Nous avons montré comment convertir une classe de service Drupal typique et une classe de contrôleur pour utiliser les propriétés promues, et comparé cela à l’approche traditionnelle. Le résultat est un code plus concis et maintenable sans sacrifier la clarté ni la fonctionnalité. À mesure que Drupal avance avec les exigences modernes de PHP, utiliser des fonctionnalités comme la promotion des propriétés dans vos modules personnalisés aidera à garder votre code propre, clair et conforme aux meilleures pratiques actuelles. Adoptez cette syntaxe moderne pour rendre votre développement Drupal à la fois plus simple et plus élégant.