Je me lance aujourd'hui sur la rédaction de 2 articles concernant certains concepts de "dynamisme" avec la classe DeriverBase de Drupal 8. Nous allons voir quelques exemples spécifiques afin de bien comprendre à quoi peut servir cette classe ainsi que le mot-clé deriver.
Nous commencerons par un exemple relativement simple concernant la génération de blocs. Le second exemple abordera un exemple un peu plus complexe, qui permettra de réellement défricher les bases à mettre en place pour obtenir un peu plus de dynamisme avec Drupal 8.
Premier exemple: les blocs
D7 et les hooks
Pour ceux qui ont travaillé avec les anciennes versions de Drupal, nombre d'entre vous connaissent les hooks que nous utilisions pour déclarer des blocs, à savoir (D7) :
- hook_block_info
- hook_block_view
- hook_block_configure
- hook_block_save
hook_block_info sert à déclarer un ou plusieurs blocs en retournant un tableau associatif dont les clés seront utilisées par hook_block_view pour créer le rendu du bloc.
L'avantage, c'est que via hook_block_info, nous pouvions créer des blocs de manière dynamique, par exemple en implémentant une boucle pour générer X blocs selon des informations paticulières.
D8 et les classes
Avec Drupal 8, la donne a changé. Faisons fi des hooks, utilisons la POO. La class Drupal\Core\Block\BlockBase est la classe à étendre pour la création d'un bloc. Nous créons une classe dans {module}/src/Plugin/Block qui étend BlockBase, et nous intégrons une annotation de type @Block pour laisser le noyau Drupal "découvrir" son existence.
Exemple basique
Un exemple très simple se trouve dans le sous-module block_example du module examples :
/** * @file * Contains \Drupal\block_example\Plugin\Block\ExampleEmptyBlock. */ namespace Drupal\block_example\Plugin\Block; use Drupal\Core\Block\BlockBase; /** * Provides a 'Example: empty block' block. * * @Block( * id = "example_empty", * admin_label = @Translation("Example: empty block") * ) */ class ExampleEmptyBlock extends BlockBase { /** * {@inheritdoc} */ public function build() { // We return an empty array on purpose. The block will thus not be rendered // on the site. See BlockExampleTest::testBlockExampleBasic(). return array(); } }
C'est un exemple très simple, qui crée un bloc vide. Nous pouvons voir l'annotation @Block avant la déclaration de classe, ainsi que l'implémentation de la fonction build pour générer le rendu final.
Comme vous pouvez le voir, il est donc facile de créer un ou plusieurs blocs qui seront ensuite disponible dans l'interface.
Problématique des blocs dynamiques
Allons maintenant un peu plus loin. Nous avons pu voir à quel point il est facile de générer un bloc en créant une classe qui hérite de BlockBase. Mais qu'en est-il lorsque nous voulons créer une série de blocs dont le rendu est issu de données particulières ?
Admettons par exemple le postulat suivant: nous avons un type d'entité configuration @ConfigEntityType nommé "Advertising" qui nous permet de créer des entités de type "publicité" ayant des données spécifiques: un titre et une image. Ce module propose également une configuration globale stockée dans la configuration YAML. Pour chaque entité créée, nous désirons avoir un bloc qui va afficher la publicité. Si nous suivons la logique de base, nous devrions créer une classe de type Plugin/Block pour chaque entité. Mais dans ce cas, nous devrions ajouter "manuellement" ces classes... Vous voyez certainement ou se situe le problème...
DeriverBase et le mot-clé deriver
C'est là que la classe DeriverBase et le mot-clé deriver entrent dans le jeu. DeriverBase est une classe abstraite, c'est à dire qu'elle ne peut être instanciée, elle est utilisée en tant que classe de base pour une sous-classe qui va l'utiliser. La sous-classe, située dans src/Plugin/Derivative, va implémenter la méthode getDerivativeDefinitions pour indiquer la logique "dynamique" et déclarer autant de "dérives" qu'il y a d'entités.
Implémentation d'une classe de dérive
Commençons par créer la classe de dérive qui va se baser sur nos entités de configuration:
namespace Drupal\advertising\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides block plugin definitions for revive ads. */ class AdvertisingBlock extends DeriverBase implements ContainerDeriverInterface { /** * The advertising storage * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $advertisingStorage; /** * The configuration factory * * @var \Drupal\Core\Config\ConfigFactoryInterface */ protected $configFactory; /** * Constructs new ReviveAdsBlock. * * @param EntityStorageInterface $revive_ads_storage * The revive ads storage. */ public function __construct(EntityStorageInterface $advertising_storage, ConfigFactoryInterface $config_factory) { $this->advertisingStorage = $advertising_storage; $this->configFactory = $config_factory; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, $base_plugin_id) { return new static( $container->get('entity_type.manager')->getStorage('advertising'), $container->get('config.factory') ); } /** * {@inheritDoc} * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinitions() */ public function getDerivativeDefinitions($base_plugin_definition) { foreach ($this->advertisingStorage->loadMultiple() as $id => $entity) { $this->derivatives[$id] = $base_plugin_definition; $this->derivatives[$id]['admin_label'] = $entity->label(); $this->derivatives[$id]['config'] = [ 'title' => $entity->label(), 'image' => $entity->getMedia(), 'link' => $entity->getLink(), 'id' => $entity->id(), ]; $this->derivatives[$id]['config_global'] = $this->configFactory->get('advertising.settings'); $this->derivatives[$id]['config_dependencies']['config'] = [$entity->getConfigDependencyName()]; } return $this->derivatives; } }
Nous utilisons l'injection de dépendances et la méthode statique create pour intégrer à notre classe le service de stockage de notre type d'entité (EntityStorageInterface), ainsi que le service de gestion de la configuration (ConfigFactoryInterface).
Si vous observez maintenant la méthode getDerivativeDefinitions, vous pouvez voir que nous itérons sur toutes les entités de type "Advertising", puis nous créons autant d'entrées dans le tableau $this->derivatives que nous avons d'entité. Pour chaque entrée, nous ajoutons, outre la clé 'admin_label', une clé 'config' qui va stocker les informations de l'entité, ainsi qu'une clé 'config_global' qui stocke la configuration globale du module.
Implémentation de la classe de bloc
Nous allons ensuite créer notre classe de bloc dans src/Plugin/Block de la manière suivante:
namespace Drupal\advertising\Plugin\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; /** * Provide a generic Advertising block. * * @Block( * id = "Advertising", * admin_label = @Translation("Advertising block"), * category = @Translation("Advertising"), * deriver = "Drupal\advertising\Plugin\Derivative\AdvertisingBlock" * ) */ class AdvertisingBlock extends BlockBase { public function build() { /** @var $config \Drupal\Core\Config\ImmutableConfig */ $config = $this->pluginDefinition['config_global']; $block = [ '#theme' => 'advertising_block', '#title' => $this->pluginDefinition['config']['title'], '#image' => $this->pluginDefinition['config']['image'], '#link' => $this->pluginDefinition['config']['link'], '#frequency' => $config->get('frequency'), ]; return $block; } }
L'information primordiale se situe au niveau de l'annotation. Comme vous le voyez, c'est là que le mot-clé deriver est utilisé, pour indiquer à Drupal que notre bloc se base sur les "dérives" implémentées dans la classe indiquée. A partir de là, il est possible de récupérer toutes les informations qui ont été intégrées dans la définition des dérives, telles que les clés 'config' et 'config_global', pour pouvoir les utiliser. Il est donc nécessaire de transmettre à la dérive toutes les informations nécessaires à la création du bloc. Ces informations peuvent être minimalistes et nous pouvons utiliser des services pour récupérer les informations plus détaillées, bien entendu.
Ici, l'exemple a été simplifié au maximum. La méthode build retourne un renderArray qui va utiliser un template twig "advertising_block". Ce dernier devra être créé et sa déclaration intégrée à l'implémentation hook_theme du module, mais ce n'est pas le sujet de cet article, le but étant de démontrer comment la classe DeriverBase peut nous permettre de rendre les choses dynamiques.
L'exemple des blocs est intéressant car c'est quelque chose d'assez récurrent lorsqu'on fait du développement. Expliquer comment utiliser DeriverBase dans cette optique me semblait un bon exemple pour débuter.
Dans le prochain article, nous verrons comment cette même classe DeriverBase peut être appliquée pour le même genre d'utilisation, mais sur d'autres éléments que les blocs.