Drupal - Comment surcharger vos nodes avec les classes de bundle

Drupal - Comment surcharger vos nodes avec les classes de bundle

Cela n'est pas franchement une nouveauté, les Classes de Bundle (Bundle Classes) ont été introduites dans le core dès Drupal 9.3 en 2021. Pourtant, je vois encore beaucoup de projets qui s'en passent, et cela fait longtemps que je voulais aborder le sujet ici.

C'est une fonctionnalité qui change la donne en termes d'architecture et de maintenabilité. Aujourd'hui, nous allons voir comment les mettre en place, en profitant de l'occasion pour utiliser la nouvelle syntaxe de hooks orientée objet disponible depuis Drupal 11 (voir à ce sujet : https://kgaut.net/blog/2024/drupal-11-utiliser-la-nouvelle-syntaxe-orientee-objet-pour-les-hooks)

Pourquoi les classes de bundle ?

De tout temps dans Drupal, le contenu est principalement représenté par des nœuds. Techniquement, un nœud de type "Article" et un nœud de type "Concert" sont tous deux des instances de la même classe générique : Drupal\node\Entity\Node.

Depuis Drupal 8, il est possible de surcharger cette classe pour y ajouter une couche métier. Imaginons que vous ayez un type de contenu "Concert" avec un champ date. Vous pourriez vouloir une méthode getDateConcert() pour récupérer cette date formatée facilement.

Le problème, c'est que si vous surchargez la classe Node générique, cette méthode getDateConcert() se retrouve disponible sur tous vos nœuds, y compris les articles de blog ou les pages statiques, ce qui n'a aucun sens sémantiquement.

C'est là qu'interviennent les Classes de Bundle. Elles permettent de corriger ce défaut en définissant une classe PHP spécifique pour un bundle précis.

Exemple d'utilisation : Concerts et Articles

Prenons un exemple concret. Dans un module personnalisé mon_module, nous voulons gérer deux types de contenus :

   Concert (concert) : Nous voulons une méthode pour récupérer facilement sa date.

   Article (article) : Nous voulons juste qu'il ait sa propre classe pour de futurs besoins.

Étape 1 : Créer les classes

Pour faire les choses proprement et avoir un socle solide, je commence par définir une classe abstraite. Elle servira de base à toutes mes classes de bundle de nœuds et contiendra les méthodes communes si besoin.

Fichier : mon_module/src/Entity/AbstractNode.php

<?php

namespace Drupal\mon_module\Entity;

use Drupal\node\Entity\Node;

/**
 * Classe abstraite de base pour nos classes de bundle.
 */
abstract class AbstractNode extends Node {
    // On pourra ajouter ici des méthodes utiles à tous nos types de contenus.
}

Ensuite, je crée la classe spécifique pour mon type de contenu "Concert". C'est ici que j'ajoute ma logique métier.

Fichier : mon_module/src/Entity/ConcertNode.php

<?php

namespace Drupal\mon_module\Entity;

use Drupal\Core\Datetime\DrupalDateTime;

/**
 * Classe spécifique pour le bundle 'concert'.
 */
class ConcertNode extends AbstractNode {

  /**
   * Retourne la date du concert sous forme d'objet DrupalDateTime.
   *
   * @return \Drupal\Core\Datetime\DrupalDateTime|null
   * La date du concert ou null si le champ est vide.
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  public function getDate(): ?DrupalDateTime {
    if ($this->hasField('field_date') && !$this->get('field_date')->isEmpty()) {
        return $this->get('field_date')->date;
    }
    return null;
  }

  /**
   * Une méthode helper pour savoir si le concert est passé.
   */
  public function isPast(): bool {
      $date = $this->getDate();
      if ($date) {
          return $date->getTimestamp() < \Drupal::time()->getRequestTime();
      }
      return FALSE;
  }

}

Je crée aussi rapidement celle pour l'article, même si elle est vide pour l'instant : 

Fichier : mon_module/src/Entity/ArticleNode.php

<?php

namespace Drupal\mon_module\Entity;

class ArticleNode extends AbstractNode {}

 

Étape 2 : Déclarer les classes (Nouvelle méthode Drupal 11)

Maintenant, il faut dire à Drupal : "Quand tu charges un nœud de type 'concert', utilise ma classe ConcertNode au lieu de la classe par défaut".

Cela se fait via le hook entity_bundle_info_alter. Utilisons la syntaxe moderne avec les attributs PHP dans une classe dédiée aux hooks :

Fichier : mon_module/src/Hook/EntityHooks.php

<?php

namespace Drupal\mon_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\mon_module\Entity\ArticleNode;
use Drupal\mon_module\Entity\ConcertNode;

/**
 * Fournit les implémentations de hooks relatives aux entités.
 */
final class EntityHooks {

  /**
   * Implements hook_entity_bundle_info_alter().
   *
   * Modifie la définition des bundles pour associer nos classes personnalisées.
   */
  #[Hook('entity_bundle_info_alter')]
  public function bundleInfoAlter(array &$bundles): void {
    // Mapping pour le type de contenu 'concert'
    if (isset($bundles['node']['concert'])) {
      $bundles['node']['concert']['class'] = ConcertNode::class;
    }
    // Mapping pour le type de contenu 'article'
    if (isset($bundles['node']['article'])) {
      $bundles['node']['article']['class'] = ArticleNode::class;
    }
  }

}

N'oubliez pas de vider les caches pour que Drupal prenne en compte le nouveau hook et les nouvelles classes !

Le résultat : une utilisation simplifiée

C'est là que la magie opère. Vous n'avez rien à changer dans votre manière de charger des entités. Drupal instanciera automatiquement la bonne classe.

Dans du code PHP (un contrôleur, un service...) :

use Drupal\node\Entity\Node;
use Drupal\mon_module\Entity\ConcertNode;

$nid = 25; // Imaginons que le nœud 25 est un concert.
$entity = Node::load($nid);

// PHP sait maintenant que $entity est une instance de ConcertNode.
if ($entity instanceof ConcertNode) {
    // On peut appeler nos méthodes personnalisées directement.
    $date = $entity->getDate();
    
    if ($entity->isPast()) {
        // Logique spécifique si le concert est passé...
    }
}

Dans Twig

C'est encore plus agréable pour les développeurs front-end. Grâce à la magie des getters dans Twig, getDate() devient accessible via node.date (si le nom ne rentre pas en conflit avec un champ) ou en appelant la méthode directement.

{# Dans node--concert.html.twig #}

<article class="concert {{ node.isPast ? 'concert--past' : 'concert--upcoming' }}">
  <h2>{{ label }}</h2>
  {# Appel direct de la méthode #}
  <p>Date du concert : {{ node.getDate().format('d/m/Y') }}</p>
  
  {# Utilisation de la méthode booléenne #}
  {% if node.isPast() %}
    <span class="badge">Concert terminé</span>
  {% endif %}
</article>

Fini les preprocess compliqués pour calculer des états ou formater des dates spécifiques !

Au-delà des Nœuds

L'exemple ici porte sur les nœuds, mais cela s'applique évidemment à l'ensemble des types d'entités de contenu : Utilisateurs, Termes de taxonomie, Media, etc.

Dans votre hook, vous pourriez ajouter :

// Dans EntityHooks::bundleInfoAlter

// Pour un vocabulaire 'tags'
if (isset($bundles['taxonomy_term']['tags'])) {
  $bundles['taxonomy_term']['tags']['class'] = TagTerm::class;
}
// Pour les utilisateurs (le bundle s'appelle aussi 'user')
if (isset($bundles['user']['user'])) {
  $bundles['user']['user']['class'] = UserCustom::class;
}

Conclusion

À titre personnel, je trouve cette fonctionnalité indispensable sur tout projet Drupal moderne. Cela permet d'ajouter toute une couche métier (getters, setters, méthodes utilitaires) directement là où elle doit être : dans l'objet qui représente la donnée.

Le code devient beaucoup plus propre, orienté objet, plus facile à tester unitairement, et bien plus maintenable sur le long terme. Si vous ne les utilisez pas encore, foncez !

Voir la présentation sur drupal.org : https://www.drupal.org/node/3191609

Contenus en rapport

Drupal 8 - Surcharger la classe de formulaire d'un nœud

Pour modifier le formulaire de création d'un nœud sous drupal 8, on peut utiliser le bon vieux HOOK_form_alter(), mais on peut aussi faire quelque chose de plus « propre » en altérant le type d'entité pour redéfinir son formulaire.

Cela se passe en deux étapes.

Drupal - Surcharger la classe de contrôle d'accès d'un type d'entité

Sous drupal 8, les types d'entités, comme les noeuds, viennent avec leur classe pour gérer le contrôle d'accès (création / modification / visualisation / suppression).

Il est possible de surcharger ces classes pour personnaliser plus finement ce contrôle.

Ajouter un commentaire

Ne sera pas publié
CAPTCHA
Désolé, pour ça, mais c'est le seul moyen pour éviter le spam...