Drupal 8 - Deriver

Drupal 8 en mode dynamique avec la classe DeriverBase - Partie 2

Tags: 

Pour la suite de cette mini-série sur la classe DeriverBase, passons maintenant à quelque chose d'un peu plus complexe afin de vous montrer à quel point DeriverBase peut être pratique.

J'ai été confronté à une problématique autrement plus difficile que celle de l'ajout de blocs dynamiques dans mes expérimentations.

Les besoins

Imaginons un module nommé "Micro Content" permettant de créer des contenus que nous voulons attacher à d'autres contenus. Ces contenus sont plus succints que des contenus tels que des articles ou des pages. Le but de notre module est de pouvoir associer des "fragments" d'informations (par exemple titre, texte, image, lien ou encore titre, texte, video) à des contenus de plus grande envergure ou à des utilisateurs. Une fois ces contenus associés, la seconde étape est de pouvoir générer des blocs qui vont afficher ces informations, sous forme de listes par exemple.

Pour donner un exemple concret, imaginez un article rédigé par un auteur sur le site, qui parle du dernier album d'un groupe connu. Nous voulons ensuite associer à cet article d'autres articles connexes rédigés par d'autres auteurs, mais ces articles connexes sont hébergés sur d'autres sites/plateformes. Avec ce système, nous pouvons créer nos associations en indiquant le titre de l'article connexe, l'auteur, le chapô, ainsi qu'une image et le lien de redirection. Lorsque toutes nos associations sont créées, un bloc va pouvoir être ajouté sur la page de l'article principal, pour lister les articles connexes et permettre de rediriger les utilisateurs vers ces derniers. Ce bloc, bien entendu, pourra être ajouté sur n'importe quel article qui embarque des associations de ce type (concept de réutilisation).

Un autre exemple serait la création de blocs permettant de rediriger vers des pages précises dans le site. Nous désirons par exemple avoir X blocs affichant une image de présentation ainsi qu'un lien sur l'image. Chaque bloc pourra être placé dans des zones du site, sur des pages pertinentes. Plutôt que de créer chaque bloc manuellement à l'aide des blocs personnalisés, notre type d'entité "Micro content" pourrait servir à ce genre de chose.

Le webmaster choisira le type de contenu "Micro content" avec le bundle correspondant à ce type de bloc (par exemple "big_picture"), et n'aura qu'a remplir les informations nécessaires pour avoir un bloc de présentation disponible.

Reprenons: nous avons besoin d'un type d'entité contenu (@ContentEntityType) à l'aide duquel nous allons créer des "bundles" qui correspondront à des catégories de fragments, chacun possédant des champs spécifiques. Le type de contenu contiendra quant à lui les champs partagés entre tous les bundles: auteur, catégorie, index, type d'entité cible (le type de contenu vers lequel notre association va pointer) et l'identifiant de l'entité cible (l'entité vers laquelle notre association va pointer).

Nous voulons pouvoir associer des "Micro Content" vers des bundles particuliers, pas forcément vers tous les bundles d'un type d'entité. Exemple: si nous parlons du type d'entité "node", des micro-contenus pourraient être créés pour des articles, mais pas pour des pages standard. Nous voulons également créer des micro-contenus que nous associerions à des utilisateurs, ou à d'autres types d'entité inclu dans le système.

Pour permettre aux administrateurs d'être efficace, nous aimerons que chaque bundle concerné par nos micro-contenus propose un onglet "Micro content" à la suite des onglets "View" et "Edit". Ces onglets, on les appelle les "Local tasks". C'est en général dans le fichier {module}.links.task.yml que nous déclarons les nouveaux onglets, en indiquant leur route (route_name), leur label (title) ainsi que leur parent (base_route). Pour être clair: en allant sur un noeud de type "article", nous pourrions voir apparaitre un onglet "Micro contents" à la suite des onglets "View" et "Edit".

Lorsque on cliquera sur l'onglet "Micro contents", nous aurons une liste de tous les micro-contenus associés au contenu principal sur lequel nous nous trouvons. Nous trouverons en haut de cette liste un bouton "Add micro-content" qui permettra d'ajouter de nouveaux micro-contenus au contenu principal.

Voilà les grandes lignes. En réalité, la conception de ce module (et de ses sous-modules) sera beaucoup plus complexe que cette introduction, mais cette dernière va vous permettre d'apréhender de manière relativement simple l'utilisation de DeriverBase pour nos besoins.

Génération du module

Commençons par utiliser Drupal Console pour générer notre nouveau module:

MacPro:drupal8 titouille$ drupal generate:module
 
 Enter the new module name:
 > Micro content
 
 Enter the module machine name [micro_content]:
 > 
 
 Enter the module Path [/modules/custom]:
 > 
 
 Enter module description [My Awesome Module]:
 > Micro content associated to other entity types
 
 Enter package name [Custom]:
 > 
 
 Enter Drupal Core version [8.x]:
 > 
 
 Do you want to generate a .module file (yes/no) [yes]:
 > yes
 
 Define module as feature (yes/no) [no]:
 > 
 
 Do you want to add a composer.json file to your module (yes/no) [yes]:
 > 
 
 Would you like to add module dependencies (yes/no) [no]:
 > 
 
 Do you confirm generation? (yes/no) [yes]:
 > 
 
Generated or updated files
 Site path: /Users/titouille/Dev/web/htdocs/sabugo/drupal8
 1 - modules/custom/micro_content/micro_content.info.yml
 2 - modules/custom/micro_content/micro_content.module
 3 - modules/custom/micro_content/composer.json
MacPro:drupal8 titouille$ 

Génération d'un type d'entité

Une fois le module généré, enchaînons tout de suite avec la génération d'un nouveau type d'entité:

MacPro:drupal8 titouille$ drupal generate:entity:content
 Enter the module name [test_module]:
 > micro_content
 
 Enter the class of your new content entity [DefaultEntity]:
 > MicroContent
 
 Enter the name of your new content entity [micro_content]:
 > 
 
 Enter the label of your new content entity [Micro content]:
 > 
 
 Enter the base-path for the content entity routes [/admin/structure]:
 > 
 
 Do you want this (content) entity to have bundles (yes/no) [no]:
 > yes
 
 Add this to your hook_theme:                                                                                           
 
   $theme['micro_content'] = array(                                                                                     
     'render element' => 'elements',                                                                                    
     'file' => 'micro_content.page.inc',                                                                                
     'template' => 'micro_content',                                                                                     
   );                                                                                                                   
 
Generated or updated files
 Site path: /Users/titouille/Dev/web/htdocs/sabugo/drupal8
 1 - modules/custom/micro_content/src/Controller/MicroContentAddController.php
 2 - modules/custom/micro_content/micro_content.permissions.yml
 3 - modules/custom/micro_content/micro_content.links.menu.yml
 4 - modules/custom/micro_content/micro_content.links.task.yml
 5 - modules/custom/micro_content/micro_content.links.action.yml
 6 - modules/custom/micro_content/src/MicroContentInterface.php
 7 - modules/custom/micro_content/src/MicroContentAccessControlHandler.php
 8 - modules/custom/micro_content/src/Entity/MicroContent.php
 9 - modules/custom/micro_content/src/MicroContentHtmlRouteProvider.php
 10 - modules/custom/micro_content/src/Entity/MicroContentViewsData.php
 11 - modules/custom/micro_content/src/MicroContentListBuilder.php
 12 - modules/custom/micro_content/src/Form/MicroContentSettingsForm.php
 13 - modules/custom/micro_content/src/Form/MicroContentForm.php
 14 - modules/custom/micro_content/src/Form/MicroContentDeleteForm.php
 15 - modules/custom/micro_content/micro_content.page.inc
 16 - modules/custom/micro_content/templates/micro_content.html.twig
 17 - modules/custom/micro_content/templates/micro-content-content-add-list.html.twig
 18 - modules/custom/micro_content/micro_content.module
 19 - modules/custom/micro_content/micro_content.module
 // generate:entity:config
 
 Enter the base-path for the config entity routes [/admin/structure]:
 > 
 
MacPro:drupal8 titouille$ 

Vous remarquerez que j'ai indiqué que mon entité doit avoir des bundles. De cette manière, Drupal Console m'a généré non seulement une entité de type contenu (src/Entity/MicroContent.php) mais également une entité de type configuration (src/Entity/MicroContentType.php). Cette dernière correspond au type d'entité, tandis que la première va correspondre à une entité appartenant à un des bundles qui seront à disposition dans le type d'entité.

Maintenant que notre base est générée, nous allons pouvoir attaquer les ajouts et modifications nécessaires pour la mise en place de notre module.

Taxonomie

Dans l'optique du module, nous avons besoin d'associer à chaque entité un ou plusieurs termes issus d'un vocabulaire, afin de "catégoriser" les contenus. Cette catégorisation rajoutera un filtre pour les entités.

Pour intégrer de manière simple une taxonomie, nous pouvons utiliser le système de configuration et le répertoire config/install, en y plaçant un fichier YAML nommé taxonomy.vocabulary.micro_content_categories.yml avec ce contenu:

langcode: fr
status: true
dependencies: {  }
name: Categories
vid: micro_content_categories
description: 'Micro content categories'
hierarchy: 0
weight: 0

Ce simple fichier va permettre de créer notre vocabulaire à l'installation du module. Nous allons également rajouter un fichier micro_content.install à la racine, qui va nous permettre de supprimer le vocabulaire lors de la désinstallation du module:

use Drupal\taxonomy\Entity\Vocabulary;
 
/**
 * Implements hook_install().
 */
function micro_content_install() {
}
 
/**
 * Implements hook_uninstall().
 */
function micro_content_uninstall() {
 
  $voc = Vocabulary::load('micro_content_categories');
  if (isset($voc)) {
    $voc->delete();
 
    // Purge field data now to allow taxonomy to be uninstalled
    // if this is the only field remaining.
    field_purge_batch(10);
  }
}

Configuration globale du module

Pour que notre module soit pleinement fonctionnel, un des pré-requis est de choisir vers quels types d'entités/bundles nos micro-contenus vont être pouvoir être associés.

Entité de type configuration

Pour ce faire, nous allons tout d'abord créer une nouvelle entité de type configuration qui permettra de stocker ces informations "par type d'entité":

namespace Drupal\micro_content\Entity;
 
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityStorageInterface;
 
/**
 * Define the DynamicContentConfiguration entity
 * 
 * @ConfigEntityType(
 *   id = "micro_content_configuration",
 *   label = @Translation("Micro content configuration"),
 *   admin_permission = "administer micro content entities",
 *   config_prefix = "micro_content_configuration",
 *   entity_keys = {
 *     "id" = "id"
 *   },
 *   list_cache_tags = { "rendered" }
 * )
 */
class MicroContentConfiguration extends ConfigEntityBase {
 
  /**
   * The id. The entity type ID
   * @var string
   */
  protected $id;
 
 
  /**
   * List of bundles to attach micro content
   * 
   * @var array
   */
  protected $bundles = [];
 
  /**
   * Constructor.
   * 
   * @param array $values
   * @param string $entity_type
   * @throws \RuntimeException
   */
  public function __contruct(array $values, $entity_type = 'micro_content_configuration') {
    if (empty($values['id'])) {
      throw new \RuntimeException('Attempt to create micro content configuration without a target entity type ID.');
    }
    parent::__construct($values, $entity_type);
  }
 
  /**
   * {@inheritDoc}
   * @see \Drupal\Core\Entity\Entity::id()
   */
  public function id() {
    return $this->id;
  }
 
  /**
   * Get the list of activated bundles for the current entity type.
   * 
   * @return array
   */
  public function getBundles() {
    return $this->bundles;
  }
 
  /**
   * Set the list of activated bundle for the current entity type.
   * 
   * @param array $bundles
   * @return \Drupal\dynamic_block\Entity\DynamicContentConfiguration
   */
  public function setBundles(array $bundles) {
    $this->bundles = $bundles;
 
    return $this;
  }
 
  /**
   * {@inheritDoc}
   * @see \Drupal\Core\Config\Entity\ConfigEntityBase::preSave()
   */
  public function preSave(EntityStorageInterface $storage) {
    //$this->id = $this->id();
    parent::preSave($storage);
  }
 
  /**
   * Indicate if the current configuration is the default configuration.
   */
  public function isDefaultConfiguration() {
    return empty($this->bundles);
  }
 
  /**
   * Load configuration by entity type ID.
   * 
   * @param string $entity_type_id
   * @return NULL|\Drupal\micro_content\Entity\MicroContentConfiguration
   */
  public static function loadByEntityType($entity_type_id) {
    static $store = [];
 
    if ($entity_type_id == NULL) {
      return NULL;
    }
 
    if (!isset($store[$entity_type_id])) {
      $config = \Drupal::entityTypeManager()->getStorage('micro_content_configuration')->load($entity_type_id);
 
      if ($config == NULL) {
        $config = static::create(['id' => $entity_type_id]);
      }
      $store[$entity_type_id] = $config;
    }
    return $store[$entity_type_id];
  }
 
  /**
   * Get form specifications to build configuration form.
   * 
   * @return array
   */
  public static function getFormSpecifications() {
    static $store = [];
    static $entity_types = [];
    static $bundles = [];
 
    if (empty($entity_types)) {
      $entity_types = \Drupal::entityTypeManager()->getDefinitions();
      $bundles = \Drupal::entityManager()->getAllBundleInfo();
    }
 
    foreach ($entity_types as $entity_type_id => $entity_type) {
      if ($entity_type instanceof ContentEntityTypeInterface 
          && $entity_type_id != 'micro_content') {
        $config = self::loadByEntityType($entity_type_id);
 
        $bundles_list = isset($bundles[$entity_type_id]) 
          ? $bundles[$entity_type_id]
          : [];
        $available_bundles = [];
 
        foreach ($bundles_list as $bundle_name => $bundle_info) {
          $available_bundles[$bundle_name] = $bundle_info['label'];
        }
        if (isset($config)) {
          $store[$entity_type_id] = [
            'label' => $entity_type->getLabel(),
            'bundles' => $available_bundles,
            'configuration' => $config
          ];
        }
      }
    }
    return $store;
  }
}

Rien de sorcier, nous avons une nouvelle entité micro_content_configuration qui propose une variable bundles ainsi que son getter. C'est dans cette variable que nous allons stocker les bundles autorisés pour un type d'entité. Chaque entité de cette classe stockera les bundles autorisés pour un type d'entité particulier, qui correspondera à la variable id.

Nous avons également quelques méthodes utiles telles que:

  • loadByEntityType: charge la configuration d'un type d'entité particulier, ou la crée si cette dernière n'existe pas encore dans le système;
     
  • getFormSpecifications: retourne la liste de tous les types d'entités ainsi que tous les bundles disponibles, accompagné par chaque configuration associée;

Avec cette entité de type configuration, nous devons associer un schema, que nous allons placer dans le fichier config/schema/micro_content_configuration.schema.yml et qui va contenir le code suivant:

micro_content.micro_content_configuration.*:
  type: config_entity
  label: 'Micro content configuration'
  mapping:
    id:
      type: string
      label: 'ID'
    bundles:
      type: sequence
      label: 'Allowed bundles'
      sequence:
        type: string
        label: 'Bundle name'

Ce schéma indique que toutes les entités de type micro_content_configuration possèdent un mapping avec un identifiant (id) ainsi qu'un tableau de bundles (bundles).

Formulaire de configuration

Drupal Console générant une foultitude de fichiers utiles, nous allons reprendre le fichier src/Form/MicroContentSettingsForm.php pour y intégrer notre configuration globale:

namespace Drupal\micro_content\Form;
 
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\micro_content\Entity\MicroContentConfiguration;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
/**
 * Class MicroContentSettingsForm.
 *
 * @package Drupal\micro_content\Form
 *
 * @ingroup micro_content
 */
class MicroContentSettingsForm extends FormBase {
 
  /**
   * 
   * @var \Drupal\Core\Routing\RouteBuilderInterface
   */
  protected $routeBuilder;
 
  /**
   * Constructor
   *
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
   */
  public function __construct(RouteBuilderInterface $route_builder) {
    $this->routeBuilder = $route_builder;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('router.builder')
    );
  }
 
  /**
   * Returns a unique string identifying the form.
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'MicroContent_settings';
  }
 
 
  /**
   * Defines the settings form for Micro content entities.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   Form definition array.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['MicroContent_settings']['#markup'] = 'Settings form for Micro content entities. Manage field settings here.';
 
    $entity_types = MicroContentConfiguration::getFormSpecifications();
    $form['selection'] = [
      '#type' => 'container',
      '#tree' => TRUE,
    ];
    foreach ($entity_types as $entity_type_id => $entity_type_info) {
      $label = $entity_type_info['label'];
      $bundles = $entity_type_info['bundles'];
      /** @var $configuration \Drupal\dynamic_block\Entity\DynamicContentConfiguration */
      $configuration = $entity_type_info['configuration'];
      $allowed_bundles = $configuration->getBundles();
 
      $form['selection'][$entity_type_id] = [
        '#type' => 'details',
        '#title' => $label,
        '#open' => !empty($allowed_bundles),
      ];
      $form['selection'][$entity_type_id]['bundles'] = [
        '#type' => 'checkboxes',
        '#options' => $bundles,
        '#default_value' => $allowed_bundles,
      ];
    }
 
    // Add submit button.
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save configuration'),
      '#button_type' => 'primary',
    ];
 
    return $form;
  }
 
  /**
   * Form submission handler.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
 
    $values = $form_state->getValues();
    $selection = $values['selection'];
    foreach ($selection as $entity_type_id => $value) {
      $bundles = array_filter($value['bundles']);
      $config = MicroContentConfiguration::loadByEntityType($entity_type_id);
 
      $config
        ->setBundles($bundles)
        ->save();
    }
    // Rebuild the menu router based on all rebuilt data.
    $this->routeBuilder->rebuild();
  }
}

Nous récupérons le service router.builder via l'injection des dépendances, ce qui va nous permettre de reconstruire les routes lors de la soumission du formulaire.

Nous implémentons ensuite la méthode buildForm pour y intégrer notre logique: nous récupérons via MicroContentConfiguration::getFormSpecifications les éléments utiles à la construction du formulaire, puis nous créons un details par type d'entité, qui va afficher une liste de cases à cocher correspondant aux bundles du type d'entité correspondant. Enfin, nous implémentons la méthode submitForm qui va permettre d'enregistrer la configuration que l'administrateur aura choisi.

Service

Nous allons également mettre en place un petit service qui nous permettra de récupérer la configuration globale du module. Pour ce faire, nous créons d'abord un fichier micro_content.services.yml à la racine du module avec ce contenu:

services:
  micro_content.config_manager:
    class: Drupal\micro_content\MicroContentConfigurationManager
    arguments: ['@entity_type.manager']

Puis la classe associée dans src/MicroContentConfigurationManager.php:

namespace Drupal\micro_content;
 
use Drupal\Core\Entity\EntityTypeManagerInterface;
 
class MicroContentConfigurationManager {
 
  /**
   * Entity type manager definition
   * 
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;
 
 
  /**
   * Constructor.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }
 
  /**
   * Retrieve configuration for micro content
   * 
   * @return array
   */
  public function getConfigurations() {
    $store = NULL;
 
    if (!isset($store)) {
      $store = [];
 
      $configurations = $this->entityTypeManager->getStorage('micro_content_configuration')->loadMultiple();
 
      foreach ($configurations as $entity_type_id => $configuration) {
        $bundles = $configuration->getBundles();
        if (!empty($bundles)) {
          $store[$entity_type_id] = array_keys($bundles);
        }
      }
    }
    return $store;
  }
}

Notre petit service va utiliser le service entity_type.manager pour implémenter la méthode getConfigurations. Cette méthode va récupérer le système de stockage des entités de type MicroContentConfiguration, itérer sur chaque configuration trouvée et remplir un tableau avec toutes les configurations qui ne sont pas vide (toutes les configurations où au moins un bundle a été sélectionné pour un type d'entité particulier).

Entité MicroContent

Nous pouvons maintenant attaquer l'entité MicroContent elle-même. Dans l'énoncé de nos besoins, nous avons indiqué que nous voulions pouvoir attacher des micro-contenus a d'autres entités. Nous devons donc stocker cette information dans notre entité, sous la forme target_type / target_id. Nous n'utiliserons pas de champ entity_reference car ce champ limite le type d'entité. La déclaration d'un champ entity_reference oblige à indiquer un target_type particulier, ce qui nous oblige à garder le même type d'entité, alors que dans notre cas, nous voulons associer les micro-contenus à différents types d'entité.

Annotation

Nous allons tout d'abord modifier l'annotation pour ajouter la possibilité de traduire nos entités et supprimer certains liens:

/**
 * Defines the Micro content entity.
 *
 * @ingroup micro_content
 *
 * @ContentEntityType(
 *   id = "micro_content",
 *   label = @Translation("Micro content"),
 *   bundle_label = @Translation("Micro content type"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\micro_content\MicroContentListBuilder",
 *     "views_data" = "Drupal\micro_content\Entity\MicroContentViewsData",
 *
 *     "form" = {
 *       "default" = "Drupal\micro_content\Form\MicroContentForm",
 *       "add" = "Drupal\micro_content\Form\MicroContentForm",
 *       "edit" = "Drupal\micro_content\Form\MicroContentForm",
 *       "delete" = "Drupal\micro_content\Form\MicroContentDeleteForm",
 *     },
 *     "access" = "Drupal\micro_content\MicroContentAccessControlHandler",
 *     "route_provider" = {
 *       "html" = "Drupal\micro_content\MicroContentHtmlRouteProvider",
 *     },
 *   },
 *   base_table = "micro_content",
+ *   data_table = "micro_content_field_data",
+ *   translatable = TRUE,
 *   admin_permission = "administer micro content entities",
 *   entity_keys = {
 *     "id" = "id",
 *     "bundle" = "type",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "uid" = "user_id",
 *     "langcode" = "langcode",
 *     "status" = "status",
 *   },
 *   links = {
 *     "canonical" = "/admin/structure/micro_content/{micro_content}",
- *     "collection" = "/admin/structure/micro_content",
- *     "add-form" = "/admin/structure/micro_content/add/{micro_content_type}",
 *     "edit-form" = "/admin/structure/micro_content/{micro_content}/edit",
 *     "delete-form" = "/admin/structure/micro_content/{micro_content}/delete",
 *   },
 *   bundle_entity_type = "micro_content_type",
 *   field_ui_base_route = "entity.micro_content_type.edit_form"
 * )
 */

Les lignes commençant par le signe "+" sont les lignes ajoutées, les lignes commençant par le signe "-" sont les lignes supprimées. Nous supprimons les chemins "collection" et "add-form" car ce sont ces chemins que nous allons rendre dynamiques, afin qu'ils puissent être liés à différents chemins plutôt qu'un chemin unique tel que c'est le cas avec cette annotation à l'origine.

Vu que nous avons supprimé le chemin "collection", nous devons également supprimer l'entrée de menu correspondante dans le fichier micro_content.links.menu.yml. Veillez à supprimer toute l'entrée correspondante à entity.micro_content.collection.

Entité

L'entité finale aura le code suivant:

namespace Drupal\micro_content\Entity;
 
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\micro_content\MicroContentInterface;
use Drupal\user\UserInterface;
 
/**
 * Defines the Micro content entity.
 *
 * @ingroup micro_content
 *
 * @ContentEntityType(
 *   id = "micro_content",
 *   label = @Translation("Micro content"),
 *   bundle_label = @Translation("Micro content type"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\micro_content\MicroContentListBuilder",
 *     "views_data" = "Drupal\micro_content\Entity\MicroContentViewsData",
 *
 *     "form" = {
 *       "default" = "Drupal\micro_content\Form\MicroContentForm",
 *       "add" = "Drupal\micro_content\Form\MicroContentForm",
 *       "edit" = "Drupal\micro_content\Form\MicroContentForm",
 *       "delete" = "Drupal\micro_content\Form\MicroContentDeleteForm",
 *     },
 *     "access" = "Drupal\micro_content\MicroContentAccessControlHandler",
 *     "route_provider" = {
 *       "html" = "Drupal\micro_content\MicroContentHtmlRouteProvider",
 *     },
 *   },
 *   base_table = "micro_content",
 *   data_table = "micro_content_field_data",
 *   translatable = TRUE,
 *   admin_permission = "administer micro content entities",
 *   entity_keys = {
 *     "id" = "id",
 *     "bundle" = "type",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "uid" = "user_id",
 *     "langcode" = "langcode",
 *     "status" = "status",
 *   },
 *   links = {
 *     "canonical" = "/admin/structure/micro_content/{micro_content}",
 *     "edit-form" = "/admin/structure/micro_content/{micro_content}/edit",
 *     "delete-form" = "/admin/structure/micro_content/{micro_content}/delete",
 *   },
 *   bundle_entity_type = "micro_content_type",
 *   field_ui_base_route = "entity.micro_content_type.edit_form"
 * )
 */
class MicroContent extends ContentEntityBase implements MicroContentInterface {
 
  use EntityChangedTrait;
 
 
 
  /**
   * {@inheritdoc}
   */
  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
    parent::preCreate($storage_controller, $values);
 
    $current_route_match = \Drupal::service('current_route_match');
    $target_entity_type_id = $current_route_match->getRouteObject()->getOption('_target_entity_type_id');
    $target_entity_id = $current_route_match->getRawParameter($target_entity_type_id);
 
    $values += array(
      'user_id' => \Drupal::currentUser()->id(),
      'target_type' => $target_entity_type_id,
      'target_id' => $target_entity_id,
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function getType() {
    return $this->bundle();
  }
 
  /**
   * {@inheritdoc}
   */
  public function getName() {
    return $this->get('name')->value;
  }
 
  /**
   * {@inheritdoc}
   */
  public function setName($name) {
    $this->set('name', $name);
    return $this;
  }
 
  /**
   * {@inheritdoc}
   */
  public function getCreatedTime() {
    return $this->get('created')->value;
  }
 
  /**
   * {@inheritdoc}
   */
  public function setCreatedTime($timestamp) {
    $this->set('created', $timestamp);
    return $this;
  }
 
  /**
   * {@inheritdoc}
   */
  public function getOwner() {
    return $this->get('user_id')->entity;
  }
 
  /**
   * {@inheritdoc}
   */
  public function getOwnerId() {
    return $this->get('user_id')->target_id;
  }
 
  /**
   * {@inheritdoc}
   */
  public function setOwnerId($uid) {
    $this->set('user_id', $uid);
    return $this;
  }
 
  /**
   * {@inheritdoc}
   */
  public function setOwner(UserInterface $account) {
    $this->set('user_id', $account->id());
    return $this;
  }
 
  /**
   * {@inheritdoc}
   */
  public function isPublished() {
    return (bool) $this->getEntityKey('status');
  }
 
  /**
   * {@inheritdoc}
   */
  public function setPublished($published) {
    $this->set('status', $published ? NODE_PUBLISHED : NODE_NOT_PUBLISHED);
    return $this;
  }
 
  /**
   * Return the targeted entity type.
   * 
   * @return string
   */
  public function getTargetEntityTypeId() {
    return $this->get('target_type')->value;
  }
 
  /**
   * Return the targeted entity ID.
   * 
   * @return number
   */
  public function getTargetEntityId() {
    return $this->get('target_id')->value;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
 
    $fields = [];
 
    $fields['id'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('ID'))
      ->setDescription(t('The ID of the Micro content entity.'))
      ->setReadOnly(TRUE);
    $fields['type'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Type'))
      ->setDescription(t('The Micro content type/bundle.'))
      ->setSetting('target_type', 'micro_content_type')
      ->setRequired(TRUE);
    $fields['uuid'] = BaseFieldDefinition::create('uuid')
      ->setLabel(t('UUID'))
      ->setDescription(t('The UUID of the Micro content entity.'))
      ->setReadOnly(TRUE);
 
    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Authored by'))
      ->setDescription(t('The user ID of author of the Micro content entity.'))
      ->setRevisionable(TRUE)
      ->setSetting('target_type', 'user')
      ->setSetting('handler', 'default')
      ->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId')
      ->setTranslatable(TRUE)
      ->setDisplayOptions('view', array(
        'label' => 'hidden',
        'type' => 'author',
        'weight' => 0,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'entity_reference_autocomplete',
        'weight' => 5,
        'settings' => array(
          'match_operator' => 'CONTAINS',
          'size' => '60',
          'autocomplete_type' => 'tags',
          'placeholder' => '',
        ),
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);
 
    $fields['name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Name'))
      ->setDescription(t('The name of the Micro content entity.'))
      ->setSettings(array(
        'max_length' => 50,
        'text_processing' => 0,
      ))
      ->setDefaultValue('')
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string_textfield',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);
 
    $fields['status'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Publishing status'))
      ->setDescription(t('A boolean indicating whether the Dynamic content is published.'))
      ->setTranslatable(TRUE)
      ->setDefaultValue(TRUE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
        'settings' => array(
          'display_label' => TRUE,
        ),
        'weight' => 1,
        ])
      ->setDisplayConfigurable('form', TRUE);
 
    $fields['langcode'] = BaseFieldDefinition::create('language')
      ->setLabel(t('Language code'))
      ->setDescription(t('The language code for the Micro content entity.'))
      ->setDisplayOptions('form', array(
        'type' => 'language_select',
        'weight' => 10,
      ))
      ->setDisplayConfigurable('form', TRUE);
 
    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created'))
      ->setDescription(t('The time that the entity was created.'));
 
    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the entity was last edited.'));
 
    $fields['target_type'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Target type'))
      ->setDescription(t('The type of entity who reference the current entity.'))
      ->setReadOnly(TRUE);
 
    $fields['target_id'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Target ID'))
      ->setDescription(t('The ID of entity who reference the current entity.'))
      ->setReadOnly(TRUE);
 
    $fields['categories'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Categories'))
      ->setDescription(t('The categories this entity belongs to.'))
      ->setSetting('target_type', 'taxonomy_term')
      ->setSetting('handler', 'default:taxonomy_term')
      ->setSetting('handler_settings', [
        'target_bundles' => [
          'micro_content_categories' => 'micro_content_categories'
        ],
        'auto_create' => FALSE,
        'auto_create_bundle' => ''
      ])
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setRequired(TRUE)
      ->setDisplayOptions('form', array(
        'type' => 'options_select',
        'weight' => 0,
      ))
      ->setDisplayConfigurable('form', TRUE);
 
      $indexes = [];
      for ($i = 0; $i <= 50; $i++) {
        $indexes[$i] = $i;
      }
    $fields['indx'] = BaseFieldDefinition::create('list_integer')
      ->setLabel(t('Index'))
      ->setDescription(t("Index of the entity. Useful if you want to create some contents on same bundle but for different cases. Leave 0 if you don't need this functionnality."))
      ->setSetting('allowed_values', $indexes)
      ->setDefaultValue(0)
      ->setDisplayOptions('form', [
        'type' => 'options_select',
        'weight' => 0,
      ])
      ->setDisplayConfigurable('form', TRUE);
 
 
    return $fields;
  }
}

Deux getter sont ajoutés: getTargetEntityTypeId et getTargetEntityId, qui permettent de récupérer les valeurs target_type et target_id. La méthode preCreate est modifiée pour inclure dans la construction d'une entité le type et l'identifiant de l'entité cible, récupérée à partir de la "route". Nous en rediscuterons un peu plus tard. Enfin, la fonction baseFieldDefinitions est modifiée comme suivant:

  • status: nous faisons en sorte que le statut soit modifiable dans le formulaire de création/édition, afin de pouvoir publier ou dépublier un micro-contenu;
     
  • target_type: nous créons un champ target_type qui est en mode lecture seule, sa valeur étant affectée dans la méthode preCreate;
     
  • target_id: nous créons un champ target_id qui est en mode lecture seule, sa valeur étant affectée dans la méthode preCreate;
     
  • categories: nous ajoutons un champ de type entity_reference pour laisser au webmaster la possibilité de choisir parmi un ou plusieurs termes de la taxonomie que nous avons intégré dans notre module;
     
  • indx: nous ajoutons un champ "index" qui va autoriser un autre filtre sur les entités dans certains cas précis. J'ai expressément utilisé "indx" plutôt que "index" car index c'est un mot-clé MySQL qui risque de poser des problèmes si il est intégré dans des requêtes SQL;

Formulaire de création/ajout

Nous allons également modifier quelque peu le formulaire de création/modification des entités MicroContent pour qu'il corresponde à nos besoins. Nous ouvrons le fichier src/Form/MicroContentForm.php et y ajoutons la méthode form:

namespace Drupal\micro_content\Form;
 
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Form controller for Micro content edit forms.
 *
 * @ingroup micro_content
 */
class MicroContentForm extends ContentEntityForm {
 
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    /* @var $entity \Drupal\micro_content\Entity\MicroContent */
    $form = parent::buildForm($form, $form_state);
    $entity = $this->entity;
 
    return $form;
  }
 
  public function form(array $form, FormStateInterface $form_state) {
 
    $form['advanced'] = [
      '#type' => 'vertical_tabs',
      '#attributes' => ['class' => ['entity-meta']],
      '#weight' => 99,
    ];
    $form['author'] = [
      '#type' => 'details',
      '#title' => t('Authoring information'),
      '#group' => 'advanced',
      '#attributes' => [
        'class' => ['form-author'],
      ],
      '#weight' => 90,
      '#optional' => TRUE,
    ];
    $form['basic_config'] = [
      '#type' => 'details',
      '#title' => t('Basic configuration'),
      '#group' => 'advanced',
      '#attributes' => [
        'class' => ['form-basic-configuration'],
      ],
      '#weight' => 0,
      '#optional' => FALSE,
    ];
 
    $form = parent::form($form, $form_state);
 
    if (isset($form['user_id'])) {
      $form['user_id']['#group'] = 'author';
    }
    if (isset($form['status'])) {
      $form['status']['#group'] = 'author';
    }
    if (isset($form['categories'])) {
      $form['categories']['#group'] = 'basic_config';
    }
    if (isset($form['indx'])) {
      $form['indx']['#group'] = 'basic_config';
    }
 
    $target_storage = $this->entityTypeManager->getStorage($this->entity->getTargetEntityTypeId());
    $target_entity = $target_storage->load($this->entity->getTargetEntityId());
    $bundle = $target_entity->bundle() != $this->entity->getTargetEntityTypeId()
    ? ' ('.$target_entity->type->entity->label().')'
        : '';
 
        $form['basic_config']['target_type_markup'] = [
          '#type' => 'item',
          '#title' => t('Target entity type'),
          '#markup' => $target_storage->getEntityType()->getLabel() . $bundle,
          '#weight' => -1,
        ];
        $form['basic_config']['target_id_markup'] = [
          '#type' => 'item',
          '#title' => t('Target entity id'),
          '#markup' => $this->entity->getTargetEntityId() . ' ('.$target_entity->label().')',
          '#weight' => -1,
        ];
 
        return $form;
  }
 
 
  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $entity = $this->entity;
    $status = parent::save($form, $form_state);
 
    switch ($status) {
      case SAVED_NEW:
        drupal_set_message($this->t('Created the %label Micro content.', [
          '%label' => $entity->label(),
        ]));
        break;
 
      default:
        drupal_set_message($this->t('Saved the %label Micro content.', [
          '%label' => $entity->label(),
        ]));
    }
    $form_state->setRedirect('entity.micro_content.canonical', ['micro_content' => $entity->id()]);
  }
}

Sur le principe, nous modifions l'aspect du formulaire pour intégrer les données de base (target_type, target_id, categories, indx, author, status) dans des onglets verticaux.

Les données target_type et target_id, qui sont des données en lecture seule, sont affichées sous forme de #markup et indiquent le type d'entité ainsi que le bundle, respectivement l'identifiant d'entité et le label.

Accès

Avant de s'occuper du routage et des dérives, nous allons mettre en place un système d'accès restreint. Souvenez-vous: nous voulons avoir un onglet "Micro contents" sur chaque bundle qui autorise d'associer un micro-contenu. Sur le principe, le routage se fera au niveau du type d'entité (node, user, ...), ce qui implique que n'importe quel bundle faisant partie d'un type d'entité autorisé affichera l'onglet. Mais nous voulons restreindre cette affichage aux bundles que nous sélectionnerons via la configuration globale. Pour ça, nous allons utiliser le système d'accès intégré à Drupal 8.

Nous rajoutons d'abord un service à notre fichier micro_content.services.yml:

services:
  micro_content.config_manager:
    class: Drupal\micro_content\MicroContentConfigurationManager
    arguments: ['@entity_type.manager']
    
  access_check.micro_content_access_checker:
    class: Drupal\micro_content\Access\MicroContentAccessCheck
    arguments: ['@entity_type.manager']
    tags:
      - { name: access_check, applies_to: _micro_content_access_check }

Ce service access_check.micro_content_access_checker, de type "access_check", va nous permettre d'appliquer nos restrictions.

Nous créons ensuite un répertoire src/Access dans lequel nous ajoutons une classe nommée MicroContentAccessCheck.php :

namespace Drupal\micro_content\Access;
 
 
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
 
/**
 * Defines an access checker for Micro content.
 */
class MicroContentAccessCheck implements AccessInterface {
 
  /**
   * The entity type manager.
   * 
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;
 
  /**
   * Constructs a DynamicContentAccessCheck object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }
 
 
  /**
   * Checks access to view/add dynamic content entities for the given route.
   *
   * @param \Symfony\Component\Routing\Route $route
   *   The route to check against.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The parametrized route.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The currently logged in account.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
 
    // Get current entity informations.
    $entity_type_id = $route->getOption('_target_entity_type_id');
    $bundles = $route->getOption('_allowed_bundles');
    $entity_id = $route_match->getRawParameter($entity_type_id);
    $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
 
    // Check if entity is from allowed bundles.
    if (!in_array($entity->bundle(), $bundles)) {
      return AccessResult::forbidden();
    }
 
    // Get type of route to do correct access check.
    $route_type = $route->getOption('_route_type');
 
    switch ($route_type) {
      case 'collection' :
 
        // Case collection, we must check entities visibility.
        $permissions = [
          'view published micro content entities', 
          'view unpublished micro content entities',
        ];
        return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
        break;
      case 'add_page' :
      case 'add_form' :
 
        // Case add, we must check entities add.
        return AccessResult::allowedIfHasPermission($account, 'add micro content entities');
        break;
    }
 
    return AccessResult::forbidden();
  }
}

Notre classe de restriction d'accès implémente AccessInterface. Elle pourra être utilisée en indiquant la clé _micro_content_access_check là ou nous désirons restreindre l'accès, tel que défini dans le service (clé applies_to).

Grosso modo, il est nécessaire d'implémenter la méthode access à laquelle nous pouvons passer une série d'arguments (non obligatoires) dont la route, la construction de la route ainsi que le compte utilisateur en cours.

Nous récupérons à partir de la route et sa construction différentes informations que nous aurons ajoutées au préalable (encore un point que nous verrons un peu plus tard) et nous effectuons nos contrôles pour savoir si l'accès est garanti ou non.

Ce sont les lignes suivantes qui sont particulièrement intéressantes:

$entity_type_id = $route->getOption('_target_entity_type_id');
$bundles = $route->getOption('_allowed_bundles');
$entity_id = $route_match->getRawParameter($entity_type_id);
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
 
    // Check if entity is from allowed bundles.
    if (!in_array($entity->bundle(), $bundles)) {
      return AccessResult::forbidden();
    }

Les 2 premières lignes permettent de récupérer des informations stockées dans la route sous forme d'options.

La 3ème ligne va récupérer l'identifiant de l'entité en cours d'évaluation. Si le type d'entité est "node", alors nous utiliserions $route_match->getRawParameter("node") pour récupérer l'identifiant du noeud en cours. Comme notre système est profondément dynamique, le type d'entité est inclu à la route et nous pouvons obtenir l'identifiant de l'entité en cours, peu importe son type.

La quatrième ligne permet de récupérer l'entité principale en cours de visualisation, puis nous faisons un simple contrôle pour savoir si le bundle de cette entité fait partie des bundles autorisés. Si ce n'est pas le cas, nous retournons un accès non-autorisé.

Avec ces quelques lignes, nous avons mis en place notre restriction au niveau bundle :-)

Dérivatives

Passons maintenant aux dérives. Nous créons le répertoire src/Plugin/Derivative et y intégrons deux classes.

LocalTasks

MicroContentLocalTasks.php:

namespace Drupal\micro_content\Plugin\Derivative;
 
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\micro_content\MicroContentConfigurationManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
class MicroContentLocalTasks extends DeriverBase implements ContainerDeriverInterface {
  use StringTranslationTrait;
 
  /**
   * The micro content configuration service.
   * 
   * @var \Drupal\micro_content\MicroContentConfigurationManager
   */
  protected $configurationManager;
 
  protected $basePluginId;
 
  /**
   * 
   * @param MicroContentConfigurationManager $micro_content_config_manager
   * @param unknown $base_plugin_id
   */
  public function __construct(MicroContentConfigurationManager $micro_content_config_manager, $base_plugin_id) {
    $this->configurationManager = $micro_content_config_manager;
    $this->basePluginId = $base_plugin_id;
  }
 
  /**
   *
   * {@inheritdoc}
   *
   * @see \Drupal\Core\Plugin\Discovery\ContainerDeriverInterface::create()
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('micro_content.config_manager'),
      $base_plugin_id
    );
  }
 
  /**
   * {@inheritDoc}
   * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinitions()
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
 
    $configurations = $this->configurationManager->getConfigurations();
    foreach ($configurations as $target_entity_type => $bundles) {
      $base_route_name = "entity.{$target_entity_type}.canonical";
 
      // Prepare the route name for the dynamic content collection overview
      $route_name = "entity.{$target_entity_type}.micro_content.collection";
      $this->derivatives[$route_name] = array(
        'entity_type' => 'micro_content',
        'target_type' => $target_entity_type,
        'title' => $this->t('Micro contents'),
        'route_name' => $route_name,
        'base_route' => $base_route_name,
      ) + $base_plugin_definition;
    }
    return parent::getDerivativeDefinitions($base_plugin_definition);
  }
 
}

Si nous décomposons un peu cette première classe de dérive, nous voyons que la méthode create est implémentée pour récupérer notre service de configuration. Nous trouvons ensuite la méthode getDerivativeDefinitions qui va itérer sur tous les types d'entités autorisés dans la configuration afin de déclarer la route suivante:

$route_name = "entity.{$target_entity_type}.micro_content.collection";

Cette simple déclaration indique que pour chaque type d'entité autorisé, nous allons avoir une route qui inclu dans son modèle le type d'entité, par exemple:

  • entity.node.micro_content.collection
  • entity.user.micro_content.collection

Pour chaque route, la route de base (base_route) correspond à la route canonique du type d'entité concerné. Pour être clair, entity.node.micro_content.collection aura comme route de base entity.node.canonical, ou si vous préférez "node/{node}" si on parle de la véritable URL finale.

Nous avons donc généré sous forme de dérive l'onglet qui devra apparaitre sur chaque entité des types autorisés.

Nous devons encore effectuer une petite modification pour prendre en considération nos dérives. Dans le fichier micro_content.links.task.yml, nous allons ajouter une nouvelle entrée qui indiquera à Drupal que certains onglets sont issus de notre classe de dérive:

# declare entity.micro_content.collection as Derivative to have a dynamic mode.
micro_content.local_tasks:
  deriver: 'Drupal\micro_content\Plugin\Derivative\MicroContentLocalTasks'
  weight: 100

Cette simple entrée indique que des localTasks sont générés à partir d'une classe de dérivation.

LocalAction

Passons maintenant aux actions avec le fichier MicroContentLocalAction.php:

namespace Drupal\micro_content\Plugin\Derivative;
 
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\micro_content\MicroContentConfigurationManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
/**
 * Provides local action definitions for all micro contents.
 */
class MicroContentLocalAction extends DeriverBase implements ContainerDeriverInterface {
 
  use StringTranslationTrait;
 
 
  /**
   * The micro content configuration service.
   * 
   * @var \Drupal\micro_content\MicroContentConfigurationManager
   */
  protected $configurationManager;
 
 
  protected $basePluginId;
 
  /**
   * Constructs a FieldUiLocalAction object.
   *
   * @param MicroContentConfigurationManager $micro_content_config_manager
   * @param unknown $base_plugin_id
   */
  public function __construct(MicroContentConfigurationManager $micro_content_config_manager, $base_plugin_id) {
    $this->configurationManager = $micro_content_config_manager;
    $this->basePluginId = $base_plugin_id;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, $base_plugin_id) {
    return new static(
      $container->get('micro_content.config_manager'),
      $base_plugin_id
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $this->derivatives = array();
 
    $configurations = $this->configurationManager->getConfigurations();
 
    foreach ($configurations as $entity_type_id => $bundles) {
      $route_name = "entity.{$entity_type_id}.micro_content.add_page";
      $this->derivatives[$route_name] = array(
        'route_name' => $route_name,
        'title' => $this->t('Add Micro content'),
        'appears_on' => array("entity.{$entity_type_id}.micro_content.collection"),
      );
    }
    foreach ($this->derivatives as &$entry) {
      $entry += $base_plugin_definition;
    }
 
    return $this->derivatives;
  }
}

La logique est la même que pour les local tasks. Nous récupérons le service de configuration et itérons sur les configurations disponibles pour créer une route du type:

$route_name = "entity.{$entity_type_id}.micro_content.add_page";

Cette route va correspondre au bouton "Add micro content" que nous trouverons sur chaque listing de micro-contenus (les routes "collection" mises en place par la première classe de dérive ci-dessus). nous voyons que la clé "appears_on" indique la page de collection que nous avons intégrée dans la classe précédente.

Pareil ici, nous devons modifier le fichier micro_content.links.action.yml afin d'indiquer que certaines entrées sont issues de notre dérive. Nous remplaçons l'entrée entity.micro_content.add_form par la suivante:

micro_content.local_actions:
  deriver: 'Drupal\micro_content\Plugin\Derivative\MicroContentLocalAction'

Routes

C'est maintenant que tout se met en place. Nous allons nous attaquer aux routes. Drupal Console a généré un fichier src/MicroContentHtmlRouteProvider.php qui détermine les routes utilisées par notre module. Nous allons le modifier pour faire en sorte que certaines routes soient dynamiques plutôt que statiques comme c'est le cas normalement.

namespace Drupal\micro_content;
 
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
 
/**
 * Provides routes for Micro content entities.
 *
 * @see Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
 * @see Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
 */
class MicroContentHtmlRouteProvider extends AdminHtmlRouteProvider {
 
  protected $targetCanonical;
  protected $targetEntityTypeId;
  protected $targetBundles;
 
  /**
   * The micro content configuration service.
   * 
   * @var \Drupal\micro_content\MicroContentConfigurationManager
   */
  protected $configurationManager;
 
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, MicroContentConfigurationManager $micro_content_config_manager) {
    parent::__construct($entity_type_manager, $entity_field_manager);
    $this->configurationManager = $micro_content_config_manager;
  }
 
 
  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('entity_field.manager'),
      $container->get('micro_content.config_manager')
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function getRoutes(EntityTypeInterface $entity_type) {
    $collection = parent::getRoutes($entity_type);
 
    $entity_type_id = $entity_type->id();
    $configurations = $this->configurationManager->getConfigurations();
 
    $this->targetEntityTypeId = $this->targetBundles = $this->targetCanonical = NULL;
 
    foreach ($configurations as $target_entity_type_id => $bundles) {
      // Get canonical pattern for the current entity type.
      $this->targetCanonical = trim($this->entityTypeManager->getStorage($target_entity_type_id)->getEntityType()->getLinkTemplate('canonical'), '/');
      $this->targetEntityTypeId = $target_entity_type_id;
      $this->targetBundles = $bundles;
 
      if ($collection_route = $this->getCollectionRoute($entity_type)) {
        $collection->add("entity.{$this->targetEntityTypeId}.{$entity_type_id}.collection", $collection_route);
      }
 
      if ($add_form_route = $this->getAddFormRoute($entity_type)) {
        if ($collection->get("entity.{$entity_type_id}.add_form")) {
          $collection->remove("entity.{$entity_type_id}.add_form");
        }
        $collection->add("entity.{$this->targetEntityTypeId}.{$entity_type_id}.add_form", $add_form_route);
      }
 
      $add_page_route = $this->getAddPageRoute($entity_type);
      if ($collection->get("entity.{$entity_type_id}.add_page")) {
        $collection->remove("entity.{$entity_type_id}.add_page");
      }
      $collection->add("entity.{$this->targetEntityTypeId}.$entity_type_id.add_page", $add_page_route);
 
    }
 
    if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) {
      $collection->add("$entity_type_id.settings", $settings_form_route);
    }
 
    return $collection;
  }
 
  /**
   * Gets the collection route.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return \Symfony\Component\Routing\Route|null
   *   The generated route, if available.
   */
  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
    if ($entity_type->hasListBuilderClass()) {
      $entity_type_id = $entity_type->id();
      $route = new Route("{$this->targetCanonical}/{$entity_type_id}");
      $route
        ->setDefaults([
          '_entity_list' => $entity_type_id,
          '_title' => "{$entity_type->getLabel()} list",
        ])
        ->setRequirement('_micro_content_access_check', 'TRUE')
        ->setRequirement($this->targetEntityTypeId, '\d+')
        ->setOption('_admin_route', TRUE)
 
        // Set some options to retrieve from route when needed.
        //  @see \Drupal\micro_content\Access\MicroContentAccessCheck
        //  @see \Drupal\micro_content\Entity\MicroContent::preCreate
        ->setOption('_target_entity_type_id', $this->targetEntityTypeId)
        ->setOption('_allowed_bundles', $this->targetBundles)
        ->setOption('_route_type', 'collection')
 
        // Add {target entity type ID} as parameter to allow
        // Drupal routing system replace token by real value.
        ->setOption('parameters', [
          $this->targetEntityTypeId => ['type' => 'entity:' . $this->targetEntityTypeId],
        ]);
 
 
      return $route;
    }
  }
 
  /**
   * Gets the add-form route.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return \Symfony\Component\Routing\Route|null
   *   The generated route, if available.
   */
  protected function getAddFormRoute(EntityTypeInterface $entity_type) {
 
    if (isset($this->targetCanonical)) {
      $entity_type_id = $entity_type->id();
 
      $route = new Route("{$this->targetCanonical}/{$entity_type_id}/add/{micro_content_type}");
      $bundle_entity_type_id = $entity_type->getBundleEntityType();
 
      $parameters = [
        $this->targetEntityTypeId => ['type' => 'entity:' . $this->targetEntityTypeId],
        $bundle_entity_type_id => ['type' => 'entity:' . $bundle_entity_type_id],
      ];
 
      // Content entities with bundles are added via a dedicated controller.
      $route
        ->setDefaults([
          '_controller' => 'Drupal\micro_content\Controller\MicroContentAddController::addForm',
          '_title_callback' => 'Drupal\micro_content\Controller\MicroContentAddController::getAddFormTitle',
        ])
        // Set custom access
        // @see \Drupal\dynamic_block\Access\DynamicContentAccessCheck
        ->setRequirement('_micro_content_access_check', 'TRUE')
 
        ->setRequirement($this->targetEntityTypeId, '\d+')
        ->setOption('_admin_route', TRUE)
 
        // Set some options to retrieve from route when needed.
        //  @see \Drupal\micro_content\Access\MicroContentAccessCheck
        //  @see \Drupal\micro_content\Entity\MicroContent::preCreate
        ->setOption('_target_entity_type_id', $this->targetEntityTypeId)
        ->setOption('_allowed_bundles', $this->targetBundles)
        ->setOption('_route_type', 'add_form')
 
        ->setOption('parameters', $parameters);
 
      return $route;
    }
  }
 
  /**
   * Gets the add page route.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return \Symfony\Component\Routing\Route|null
   *   The generated route, if available.
   */
  protected function getAddPageRoute(EntityTypeInterface $entity_type) {
    $route = new Route("/{$this->targetCanonical}/{$entity_type->id()}/add");
    $route
      ->setDefaults([
        '_controller' => 'Drupal\micro_content\Controller\MicroContentAddController::add',
        '_title' => "Add {$entity_type->getLabel()}",
      ])
 
      // Set custom access
      // @see \Drupal\dynamic_block\Access\DynamicContentAccessCheck
      ->setRequirement('_micro_content_access_check', 'TRUE')
 
      ->setRequirement($this->targetEntityTypeId, '\d+')
      ->setOption('_admin_route', TRUE)
 
      // Set some options to retrieve from route when needed.
      //  @see \Drupal\micro_content\Access\MicroContentAccessCheck
      //  @see \Drupal\micro_content\Entity\MicroContent::preCreate
      ->setOption('_target_entity_type_id', $this->targetEntityTypeId)
      ->setOption('_allowed_bundles', $this->targetBundles)
      ->setOption('_route_type', 'add_page')
 
      ->setOption('parameters', [
        $this->targetEntityTypeId => ['type' => 'entity:' . $this->targetEntityTypeId],
      ]);
 
    return $route;
  }
 
  /**
   * Gets the settings form route.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   *
   * @return \Symfony\Component\Routing\Route|null
   *   The generated route, if available.
   */
  protected function getSettingsFormRoute(EntityTypeInterface $entity_type) {
    $route = new Route("/admin/structure/{$entity_type->id()}/settings");
    $route
      ->setDefaults([
        '_form' => 'Drupal\micro_content\Form\MicroContentSettingsForm',
        '_title' => "{$entity_type->getLabel()} settings",
      ])
      ->setRequirement('_permission', $entity_type->getAdminPermission())
      ->setOption('_admin_route', TRUE);
 
    return $route;
  }
}

Observons d'abord la méthode getRoutes. Nous récupérons la configuration via le service injecté, puis nous itérons sur chaque type d'entité. Dans cette boucle, nous stockons 3 variables à chaque itération:

  • targetCanonical: c'est le modèle d'URL canonique du type d'entité en cours, par exemple "node/{node}" ou "user/{user}". Ce modèle va être utilisé pour la création des routes. L'avantage d'utiliser ce modèle, c'est que peu importe si le type d'entité en cours est "noeud", "user" ou n'importe quel autre type d'entité, nous pouvons constuire notre URL de manière propre. Imaginez par exemple que le type d'entité possède un modèle du type "admin/structure/{entity_type}/{entity}", nous aurons toujours une URL correcte pour ajouter l'onglet listant la collection de micro-contenus.
     
  • targetEntityTypeId: c'est le type d'entité en cours, par exemple "node" ou "user".
     
  • targetBundles: ce sont les bundles autorisés pour les micro-contenus. Comme expliqué au départ, il sera possible, via la configuration globale, d'associer des micro-contenus à certains bundles particuliers des types d'entités, pas forcément à tous. Nous gardons en mémoire lors de chaque itération les bundles autorisés afin de les inclure à chaque route, comme nous allons le voir.

A la suite de ce stockage, nous créons les routes en faisant appel aux méthodes correspondantes à chaque type de route. Concentrons nous simplement sur la route "collection", les autres utilisant le même principe. D'une route avec le modèle

entity.{$entity_type_id}.collection 
// ie: entity.micro_content.collection

Nous passons à une route avec le modèle

entity.{$this->targetEntityTypeId}.{$entity_type_id}.collection 
// ie: entity.node.micro_content.collection

Cette nouvelle route inclu le type d'entité cible, et nous avons ce modèle pour chaque type d'entité autorisé.

Si nous observons maintenant la méthode getCollectionRoute, nous pouvons y voir les choses suivantes:

La route ($route) possède le modèle défini ci-après:

$route = new Route("{$this->targetCanonical}/{$entity_type_id}");

Si nous prenons l'exemple du type d'entité cible "node", ce modèle donnera "node/{node}/micro_content".

Les valeurs par défaut sont ensuite affectées avec les clés _entity_list et _title:

        ->setDefaults([
          '_entity_list' => $entity_type_id,
          '_title' => "{$entity_type->getLabel()} list",
        ])

Nous trouvons un setRequirement qui indique les contrôles d'accès à effectuer. Souvenez-vous, nous avons créé un service de contrôle d'accès qui utilise la classe src/Access/MicroContentAccessCheck.php et qui est "instanciable" via la clé _micro_content_access_check. C'est ici que nous utilisons ce mot-clé, afin d'indiquer que la route doit passer par ce contrôle pour être valide:

        ->setRequirement('_micro_content_access_check', 'TRUE')

Une première option est intégrée, indiquant l'expression régulière à valider pour le type d'entité. En fait, le type d'entité (par exemple {node}) sera remplacé par l'identifiant du noeud en cours, et devra être de type numérique (\d+). Ce pré-requis est lié aux paramètres indiqués plus bas.

        ->setRequirement($this->targetEntityTypeId, '\d+')

Une seconde option est intégrée, indiquant que cette route est une route de type "administration" et devra donc utiliser le thème d'administration.

        ->setOption('_admin_route', TRUE)

Puis vient une suite d'options qui vont nous servir, par exemple dans la classe de contrôle d'accès. C'est là que c'est intéressant. Il est possible d'intégrer aux routes des "variables" que nous pouvons enuite récupérer pour le traitement. Ici, nous intégrons le type d'entité (_target_entity_type_id), les bundles autorisés (_allowed_bundles) ainsi que le type de route (_route_type).

        // Set some options to retrieve from route when needed.
        //  @see \Drupal\micro_content\Access\MicroContentAccessCheck
        //  @see \Drupal\micro_content\Entity\MicroContent::preCreate
        ->setOption('_target_entity_type_id', $this->targetEntityTypeId)
        ->setOption('_allowed_bundles', $this->targetBundles)
        ->setOption('_route_type', 'collection')

Reprenez la classe de contrôle d'accès MicroContentAccessCheck ou la méthode de l'entité MicroContent::preCreate afin de voir comment ces options sont récupérées et utilisées ;-)

Enfin, nous passons un tableau de paramètres qui détermine les paramètres de l'URL:

        // Add {target entity type ID} as parameter to allow
        // Drupal routing system replace token by real value.
        ->setOption('parameters', [
          $this->targetEntityTypeId => ['type' => 'entity:' . $this->targetEntityTypeId],
        ]);

Ici, un seul paramètre intervient, qui est une sorte de token qui sera remplacé par une classe de type ParamConverter. le modèle 

[$entity_type_id => 'entity:' . $entity_type_id]
// exemple: ['node' => 'entity:node']

permet de remplacer le token par l'identifiant d'entité correspondant. Dans d'autres méthodes de construction des routes, plusieurs paramètres interviennent, je vous laisse y jeter un oeil.

Avec ces modifications, nous générons toutes les routes et les chemins nécessaires au bon fonctionnement de notre module. Encore quelques étapes et nous verrons le bout du tunnel.

ListBuilder

Nous avons mis en place nos routes, il faut encore travailler sur le formulaire qui affiche les entités de type micro-contenus.

Si le but est d'avoir un onglet "Micro contents" sur d'autres entités qui permet de lister les micro-contenus attachés, alors il faut aussi faire en sorte que cette liste soit filtrée pour n'afficher que les micro-contenus associés au contenu principal en cours. Nous allons modifier la classe MicroContentListBuilder pour intégrer ce filtre:

namespace Drupal\micro_content;
 
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Core\Routing\LinkGeneratorTrait;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
/**
 * Defines a class to build a listing of Micro content entities.
 *
 * @ingroup micro_content
 */
class MicroContentListBuilder extends EntityListBuilder {
 
  use LinkGeneratorTrait;
 
  /**
   * The current route match service.
   *
   * @var \Drupal\Core\Routing\CurrentRouteMatch
   */
  protected $currentRouteMatch;
 
  /**
   * The current target entity type (resolved by CurrentRouteMatch)
   *
   * @var string
   */
  protected $targetEntityTypeId;
  /**
   * The current target entity id (resolved by CurrentRouteMatch)
   *
   * @var integer
   */
  protected $targetEntityId;
 
  /**
   * Constructs a new EntityListBuilder object.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The entity storage class.
   */
  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, CurrentRouteMatch $current_route_match) {
    parent::__construct($entity_type, $storage);
    $this->currentRouteMatch = $current_route_match;
 
    // Store current target entity type id and target entity id.
    $this->targetEntityTypeId = $this->currentRouteMatch->getRouteObject()->getOption('_target_entity_type_id');
    $this->targetEntityId = $this->currentRouteMatch->getRawParameter($this->targetEntityTypeId);
  }
 
  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static(
        $entity_type,
        $container->get('entity.manager')->getStorage($entity_type->id()),
        $container->get('current_route_match')
        );
  }
 
  /**
   * Loads entity IDs using a pager sorted by the entity id
   * and filtered by target entity type/target entity id.
   *
   * Mimic the original method from parent but add
   * some new conditions to filter list.
   *
   * @return array
   *   An array of entity IDs.
   */
  protected function getEntityIds() {
    $query = $this->getStorage()->getQuery()
    ->sort($this->entityType->getKey('id'));
 
    $query
    ->condition('target_type', $this->targetEntityTypeId)
    ->condition('target_id', $this->targetEntityId);
 
    // Only add the pager if a limit is specified.
    if ($this->limit) {
      $query->pager($this->limit);
    }
    return $query->execute();
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildHeader() {
 
    $header = [];
 
    $header['id'] = $this->t('Micro content ID');
    $header['name'] = $this->t('Name');
    $header['bundle'] = $this->t('Bundle');
 
    return $header + parent::buildHeader();
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildRow(EntityInterface $entity) {
 
    $row = [];
 
    /* @var $entity \Drupal\micro_content\Entity\MicroContent */
    $row['id'] = $entity->id();
    $row['name'] = $this->l(
      $entity->label(),
      new Url(
        'entity.micro_content.edit_form', array(
          'micro_content' => $entity->id(),
        )
      )
    );
    // @see http://drupal.stackexchange.com/questions/187980/how-to-get-bundle-label-from-entity
    // We use entity metadata wrapper to retrieve the type referenced and get label.
    $row['bundle'] = $entity->type->entity->label();
 
    return $row + parent::buildRow($entity);
  }
}

Ici, les éléments principaux sont:

  • L'injection de dépendance via la méthode createInstance pour intégrer le service current_route_match;
     
  • La récupération du type d'entité ainsi que de l'identifiant d'entité via la route en cours dans le constructeur;
     
  • L'implémentation (ou plutôt surdéfinition) de la méthode getEntityIds pour y ajouter deux conditions sur le type d'entité et l'identifiant d'entité;

Ces quelques modifications vont permettre d'avoir une liste filtrée uniquement sur les micro-contenus associés à l'entité principale en cours d'affichage.

Formulaire d'ajout d'un bundle

Le dernier point concerne l'ajout des entités. Lorsqu'on utilise une entité de type contenu (@ContentEntityBase) qui peut être décliné en bundles, une entité de type configuration (@ConfigEntityBase) est générée par Drupal Console.

Cette entité de type configuration est accompagnée par un système de formulaire permettant de créer des bundles associés au type d'entité, bundles auxquels nous pouvons rattacher des champs, bien entendu.

Le fait d'avoir des bundles implique une problématique assez simple. Si nous prenons notre entité MicroContent et son bouton "Add Micro content", lorsque nous cliquons sur ce dernier, le système doit nous proposer de quel bundle fera partie l'entité que nous allons créer. En gros, un formulaire intermédiaire est proposé, qui liste les bundles disponibles, afin de laisser à l'auteur le choix du bundle.

Ce processus est intégré dans la classe src/Controller/MicroContentAddController.php, qui détermine deux callbacks:

  • add: callback qui va proposer le formulaire intermédiaire permettant de choisir parmi les différents bundles celui qui sera la base de notre entité;
     
  • addForm: callback final, qui va afficher le formulaire de création de l'entité, en indiquant le bundle sélectionné par l'utilisateur.

Nous allons modifier le callback add afin de faire en sorte que la construction des liens correspondent à nos besoins:

namespace Drupal\micro_content\Controller;
 
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
 
 
/**
 * Class MicroContentAddController.
 *
 * @package Drupal\micro_content\Controller
 */
class MicroContentAddController extends ControllerBase {
 
  public function __construct(EntityStorageInterface $storage, EntityStorageInterface $type_storage) {
    $this->storage = $storage;
    $this->typeStorage = $type_storage;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    /** @var EntityTypeManagerInterface $entity_type_manager */
    $entity_type_manager = $container->get('entity_type.manager');
    return new static(
      $entity_type_manager->getStorage('micro_content'),
      $entity_type_manager->getStorage('micro_content_type')
    );
  }
  /**
   * Displays add links for available bundles/types for entity micro_content .
   *
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   The parametrized route.
   *
   * @return array
   *   A render array for a list of the micro_content bundles/types that can be added or
   *   if there is only one type/bundle defined for the site, the function returns the add page for that bundle/type.
   */
  public function add(RouteMatchInterface $route_match) {
 
    $target_entity_type_id = $route_match->getRouteObject()->getOption('_target_entity_type_id');
    $target_entity_id = $route_match->getRawParameter($target_entity_type_id);
 
    $types = $this->typeStorage->loadMultiple();
    if ($types && count($types) == 1) {
      $type = reset($types);
      return $this->addForm($type, $request);
    }
    if (count($types) === 0) {
      return array(
        '#markup' => $this->t('You have not created any %bundle types yet. @link to add a new type.', [
          '%bundle' => 'Micro content',
          '@link' => $this->l($this->t('Go to the type creation page'), Url::fromRoute('entity.micro_content_type.add_form')),
        ]),
      );
    }
    return array(
      '#theme' => 'micro_content_content_add_list', 
      '#content' => $types,
      '#target_entity_type_id' => $target_entity_type_id,
      '#target_entity_id' => $target_entity_id,
    );
  }
 
  /**
   * Presents the creation form for micro_content entities of given bundle/type.
   *
   * @param EntityInterface $micro_content_type
   *   The custom bundle to add.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   *
   * @return array
   *   A form array as expected by drupal_render().
   */
  public function addForm(EntityInterface $micro_content_type, Request $request) {
    $entity = $this->storage->create(array(
      'type' => $micro_content_type->id()
    ));
    return $this->entityFormBuilder()->getForm($entity);
  }
 
  /**
   * Provides the page title for this controller.
   *
   * @param EntityInterface $micro_content_type
   *   The custom bundle/type being added.
   *
   * @return string
   *   The page title.
   */
  public function getAddFormTitle(EntityInterface $micro_content_type) {
    return t('Create of bundle @label',
      array('@label' => $micro_content_type->label())
    );
  }
}

Dans cette méthode add, nous avons modifié le paramètre passé. C'était un objet de type Symfony\Component\HttpFoundation\Request et nous l'avons remplacé par un objet de type \Drupal\Core\Routing\RouteMatchInterface.

Cette modification nous autorise à récupérer des informations à partir de la route paramétrée qui mène au callback. Nous pouvons donc extraire le type d'entité ainsi que son identifiant, que nous allons pouvoir passer à la méthode de rendu utilisée en retour de la méthode add.

Vu que nous avons passé deux nouvelles variables à la méthode de rendu, nous devons les intégrer dans l'implémentation de hook_theme du module:

  $theme['micro_content_content_add_list'] = [
    'render element' => 'content',
    'variables' => [
      'content' => NULL,
      'target_entity_type_id' => NULL,
      'target_entity_id' => NULL,
    ],
    'file' => 'micro_content.page.inc',
  ];

Et nous allons modifier la méthode de rendu template_preprocess_micro_content_content_add_list située dans le fichier micro_content.page.inc:

/**
 * @file
 * Contains micro_content.page.inc.
 *
 * Page callback for Micro content entities.
 */
 
use Drupal\Core\Render\Element;
use Drupal\Core\Link;
use Drupal\Core\Url;
 
/**
 * Prepares variables for Micro content templates.
 *
 * Default template: micro_content.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An associative array containing the user information and any
 *   - attributes: HTML attributes for the containing element.
 */
function template_preprocess_micro_content(array &$variables) {
  // Fetch MicroContent Entity Object.
  $micro_content = $variables['elements']['#micro_content'];
 
  // Helpful $content variable for templates.
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
}
 
/**
* Prepares variables for a custom entity type creation list templates.
*
* Default template: micro_content-content-add-list.html.twig.
*
* @param array $variables
*   An associative array containing:
*   - content: An array of micro_content-types.
*
* @see block_content_add_page()
*/
function template_preprocess_micro_content_content_add_list(&$variables) {
  $variables['types'] = array();
 
  // Retrieve target type and id.
  $target_entity_type_id = $variables['target_entity_type_id'];
  $target_entity_id = $variables['target_entity_id'];
 
  // Get query parameters.
  $query = \Drupal::request()->query->all();
 
  // Iterate on each content and build link(s).
  foreach ($variables['content'] as $type) {
 
    // Prepare Url parameters and build Url from dynamic route.
    $route = "entity.$target_entity_type_id.micro_content.add_form";
    $route_parameters = [
      'micro_content_type' => $type->id(),
      $target_entity_type_id => $target_entity_id,
    ];
    $options = [
      'query' => $query,
    ];
 
    $url = new Url($route, $route_parameters, $options);
 
    $variables['types'][$type->id()] = [
      'link' => Link::fromTextAndUrl($type->label(), $url),
      'description' => [
        '#markup' => $type->label(),
      ],
      'title' => $type->label(),
      'localized_options' => [
        'query' => $query,
      ],
    ];
  }
}

Et le tour et joué.

Tests

A priori, la boucle est bouclée. Nous avons implémenté tous les éléments de base pour que notre module soit fonctionnel. Il ne reste plus qu'à tester tout ça histoire de voir si la logique a été respectée.

Activation du module

Via Drupal Console, bien entendu:

MacPro:drupal8 titouille$ drupal module:install micro_content
 Installing module(s) micro_content
 
 [OK] The following module(s) were installed successfully: micro_content                                                
 
 // cache:rebuild
 
 Rebuilding cache(s), wait a moment please.
 
 [OK] Done clearing cache(s).                                                                                           
 
MacPro:drupal8 titouille$ 

Configuration globale

Nous naviguons sur la page admin/structure/micro_content/settings et nous sélectionnons les bundles qui sont autorisés à être la cible de micro-contenus. Pour ma part, node:article et user:user:

Ajout de bundles

Nous pouvons ensuite ajouter un ou plusieurs bundles sur notre type d'entité micro_content en passant par la page admin/structure/micro_content_type. Nous rajoutons deux bundles, histoire de voir si le formulaire intermédiaire qui permet de sélectionner le bundle est correctement mis en place.

Contrôle de l'affichage de l'onglet

Nous pouvons maintenant naviguer sur une page de type article, une autre page de type page et une page de type utilisateur pour se rendre compte que l'onglet "Micro contents" s'affiche bien sur les bundles selectionnés uniquement. La classe de contrôle d'accès fait donc correctement son travail en limitant l'affichage de l'onglet selon les choix effectués dans la configuration globale du module.

Ajout de micro contenus

Premier test, nous pouvons tenter d'accéder à la liste d'un contenu qui n'autorise pas les micro-contenus (en naviguant par exemple vers node/XYZ/micro_content" ou XYZ correspond à l'identifiant d'un noeud issu d'un bundle qui n'autorise pas les micro-contenus). Nous nous retrouvons devant une page de type "access denied".

Si nous allons maintenant sur un contenu qui autorise les micros-contenus et que nous cliquons sur l'onglet "Micro contents", nous devrions tomber sur une liste vide, avec un bouton "Add Micro content" tel que ci-dessous:

Si nous cliquons sur le bouton "Add Micro content", nous devrions avoir une liste de bundles disponibles, tels que ci-dessous. Si c'est le cas, alors l'implémentation du formulaire intermédiaire est correcte. Si ce n'est pas le cas, c'est peut-être que vous n'avez créé qu'un seul bundle pour les entités micro_content. Dans ce cas là, vous êtes directement redirigé vers la création de l'entité, qui utilisera le seul bundle disponible.

Nous pouvons choisir le style de l'entité à créer en sélectionnant parmi les bundles disponibles. Créons deux entités sur un noeud, puis une entité sur un utilisateur, afin de tester le filtrage des entités dans la liste de chaque contenu principal. Il va certainement être nécessaire d'ajouter un ou deux termes dans le vocabulaire qui a été créé par le module.

Liste des micro-contenus sur un noeud:

Liste des micro-contenus sur un utilisateur:

Nous pouvons donc constater que les listes sont différentes selon l'entité principale (ici, noeud versus utilisateur).

La logique du module semble fonctionnelle telle qu'elle avait été définie dans les besoins.

Conclusion

Cet article a intégré beaucoup de code pour arriver au but, mais les idées principales résident dans les concepts suivants:

  • Création d'une entité de type configuration pour stocker la configuration globale du module;
     
  • Utilisation d'une entité de type contenu + entité de type configuration pour obtenir une association type d'entité/bundle;
     
  • Intégration dans l'entité de type contenu de divers champs, dont target_type et target_id pour stocker les informations de l'entité principale à associer avec nos entités micro-contenus. La valorisation de ces deux champs se fait via la méthode preCreate afin de fixer définitivement les valeurs dès le départ;
     
  • Réécriture des routes pour prendre en compte des routes en relation avec l'entité principale, afin d'intégrer un nouvel onglet sur toutes les entités autorisées;
     
  • Utilisation de DeriverBase pour rendre les "localTasks" et les "LocalActions" dynamiques;
     
  • Utilisation d'un contrôle d'accès personnalisé pour limiter l'affichage des onglets et l'accessibilité aux actions uniquement sur les bundles autorisés;
     
  • Modification du listBuilder pour ajouter un filtrage à la requête de récupération des données avant affichage;
     
  • Modification du contrôleur permettant de générer le formulaire d'ajout d'entité pour réécrire correctement les url's de destination;

Nous avons pu constater qu'il est possible, avec une bonne compréhension de ces concepts, de développer des modules intégrant une bonne complexité et un certain dynamisme. Cet exemple ouvre la voie à de nombreuses applications.

Le module micro_content est encore loin d'être finalisé. Le but ultime est de générer des champs, widgets et formateurs personnalisés à utiliser avec les différents bundles, puis de développer un sous-module qui va permettre la construction dynamique de blocs d'affichages réutilisables selon leur emplacement, accompagnés d'un système de configuration associable aux blocs selon des critères spécifiques, mais ce n'était pas le but de cet article.

J'espère que ces explications vous permettrons de vous lancer sur la création de modules complexes ;-)

A bientôt pour de nouvelles aventures !

Ajouter un commentaire

CAPTCHA
Cette question permet de savoir si vous êtes un visiteur ou un robot de soumission de spams