Créer une entité Drupal 8 en 10 secondes top chrono

Compteur vitesse voiture

Dans un précédent billet, nous avons découvert le projet Console qui permet d'automatiser la création de modules Drupal 8 et d'autres taches récurrentes. Découvrons ensemble quelques autres fonctionnalités très intéressantes.

Les entités sont au coeur de l'architecture Drupal, et encore plus dans Drupal 8 où tout (ou presque) est une entité. Les Noeuds, Users, Termes de taxonomies (pour ne citer que les plus connues) de Drupal sont des entités. Créer une nouvelle entité peut permettre de répondre à des fonctionnalités spécifiques en implémentant au sein de celle-ci sa propre logique métier, et / ou encore de répondre à une problématique de performance en implémentant dans la table de base de l'entité toutes ses propriétés métier, évitant ainsi de nombreuses et coûteuses requêtes pour générer et rendre une (ou plusieurs dizaines de milliers) instance de cette entité.

Création d'une entité Drupal 8 en 10 secondes

Si vous n'avez pas encore installé le projet Console, je vous invite à consulter ce billet Créer un module Drupal 8 en moins de 30 secondes. Nous vous attendons.

Executons la commande suivante

cd /path/to/drupal8folder
bin/console generate:entity:content

 

Nous déclarons notre nouvelle entité dans notre prédécent module créé, intitulé Example, et donnons un nom à notre entité et sa classe.

En 3 questions, et moins de 10 secondes, nous venons de générer tout le code pour implémenter une nouvelle entité.

Regardons de plus près le résultat obtenu.

structure folder of an drupal 8 entity

Outre avoir mis à jour le fichier de déclaration des routes (example.routing.yml) et les fichiers de lien (example.links.menu.yml, etc), la commande a généré les fichiers suivants :

  • src/NoteInterface.php qui déclare l'interface de notre entité
  • src/NoteAccessControlHandler.php qui contrôle les droits d'accès à notre entité
  • src/Entity/Note.php qui définit la classe de notre entité
  • src/Entity/Form/NoteSettingsForm.php qui définit le formulaire de paramètres de l'entité
  • src/Entity/Form/NoteForm.php qui définit le formulaire de création ou d'édition de l'entité
  • src/Entity/Form/NoteDeleteForm.php qui définit le formulaire de suppression de l'entité
  • src/Entity/Controller/NoteListController.php qui fournit la liste de nos entités créées

Activons notre module pour consulter les différentes interfaces fournies

Formulaire de gestion des champs de notre entité Drupal 8

Nous pouvons ajouter autant de champs que nécessaire à notre nouvelle entité et également gérer l'affichage du formulaire et/ou celui du rendu depuis les onglets correspondants.

Liste des nos entités drupal 8 créées

Nous pouvons consulter, créer, modifier ou supprimer nos entités depuis les chemins fournis par défaut, que nous pouvons bien sûr modifier depuis le fichier example.routing.yml

Bref nous disposons désormais de notre propre entité qui bénéficie de base de toute la puissance de Drupal 8 au travers de son API, notamment la field API et la form API.

Intégration de notre entité drupal 8 avec Views

Console ne propose pas (pour l'instant) "out of the box" l'intégration de notre nouvelle entité avec Views. (mise à jour du 24/10/2014 : Console propose depuis la version 0.2.16 l'intégration avec Views des entités générées. Cf. Integration with Views for entity content generated)

Il nous suffit d'implémenter \Drupal\views\EntityViewsDataInterface dans notre Classe Note et de le déclarer dans les annotations au moyen de la propriété views_data.

Dans notre fichier Note.php généré par Console, ajoutons cette ligne

"views_data" = "Drupal\example\Entity\NoteViewsData",

Pour obtenir les annotations suivantes

 /**
 * @file
 * Contains Drupal\example\Entity\Note.
 */

namespace Drupal\example\Entity;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\example\NoteInterface;
use Drupal\user\UserInterface;

/**
 * Defines the Note entity.
 *
 * @ingroup example
 *
 * @ContentEntityType(
 *   id = "note",
 *   label = @Translation("Note entity"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\example\Entity\Controller\NoteListController",
 *     "views_data" = "Drupal\example\Entity\NoteViewsData",
 *
 *     "form" = {
 *       "add" = "Drupal\example\Entity\Form\NoteForm",
 *       "edit" = "Drupal\example\Entity\Form\NoteForm",
 *       "delete" = "Drupal\example\Entity\Form\NoteDeleteForm",
 *     },
 *     "access" = "Drupal\example\NoteAccessControlHandler",
 *   },
 *   base_table = "note",
 *   admin_permission = "administer Note entity",
 *   fieldable = TRUE,
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid"
 *   },
 *   links = {
 *     "edit-form" = "note.edit",
 *     "admin-form" = "note.settings",
 *     "delete-form" = "note.delete"
 *   },
 *   field_ui_base_route = "note.settings"
 * )
 */ 

Et créons notre fichier NoteViewsData.php (dans le répertoire src/Entity) qui va implémenter EntityViewsDataInterface, et étendre EntityViewsData, pour disposer automatiquement des champs de base de notre entité (id, name, uuid, etc.). Vous trouverez toute les explications et plus encore dans la documentation de l'Entity API de Drupal 8.

 /**
 * @file
 * Contains Drupal\example\Entity\NoteViewsData.
 */

namespace Drupal\example\Entity;

use Drupal\views\EntityViewsData;
use Drupal\views\EntityViewsDataInterface;

/**
 * Provides the views data for the node entity type.
 */
class NoteViewsData extends EntityViewsData implements EntityViewsDataInterface {

  /**
   * {@inheritdoc}
   */
  public function getViewsData() {
    $data = parent::getViewsData();

    $data['note']['table']['base'] = array(
      'field' => 'id',
      'title' => t('Note'),
      'help' => t('The note entity ID.'),
    );

    return $data;
  }
} 

Notre entité est alors disponible en tant que telle dans Views. Ainsi que tous les champs qui lui seront ajoutés (dans l'exemple ci-dessous un champ décimal intitulé Note a été rajouté à notre entité)

Views et notre custom entité drupal 8

 

Views et notre entité drupal 8

Pour finir l'intégration complète de notre entité avec Views, et pouvoir par exemple utiliser le mode de rendu de notre entité pour l'affichage de la vue, il nous faut fournir un template à notre entité. Nous allons donc modifier notre fichier example.module pour implémenter la fonction hook_theme.

 /**
 * Implements hook_theme().
 */
function example_theme()
{
  $theme = [];
  $theme['note'] = array(
    'render element' => 'elements',
    'file' => 'note.page.inc',
    'template' => 'note',
  );

  return $theme;
} 

Nous implémentons avec cette fonction le template note.html.twig et nous utilisons le fichier note.page.inc pour fournir les variables à notre template. Créons nos deux fichiers, note.html.twig dans le répertoire template de notre module, et note.page.inc à la racine.

 {#
/**
 * @file note.html.twig
 * Default theme implementation to present note data.
 *
 * This template is used when viewing a note entity's page,
 *
 *
 * Available variables:
 * - content: A list of content items. Use 'content' to print all content, or
 * - attributes: HTML attributes for the container element.
 *
 * @see template_preprocess_note()
 *
 * @ingroup themeable
 */
#}
<div {{ attributes.addClass("note") }}>
  {% if content %}
    {{- content -}}
  {% endif %}
</div>

Et le fichier note.page.inc

 /**
 * @file note.page.inc
 * Note page callback file for the note entity.
 */

use Drupal\Core\Render\Element;

/**
 * Prepares variables for note templates.
 *
 * Default template: note.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_note(&$variables) {

  // Helpful $content variable for templates.
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
} 

Notre entité est désormais pleinement opérationnelle.

Ajouter des propriétés à une entité Drupal 8 spécifique

Pour aller un peu plus loin encore, nous allons ajouter quelques propriétés spécifiques à notre table de base de notre entité. Ce cas de figure peut être intéressant pour des raisons de performance si vous prévoyez d'avoir plusieurs dizaines de milliers (ou plus) de lignes à rendre. Ainsi les valeurs seront lues directement depuis la table de base évitant de couteuses jointures dans la requête.

Ajoutons à notre entité trois propriétés : une note littérale, une appréciation littérale et enfin l'élève (sous la forme d'un champ Entity Reference ciblant les utilisateurs enregistrés). Pour ce nous les déclarons dans la fonction baseFieldDefinitions de notre fichier Note.php implémentant la classe de notre entité.

   /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields['id'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('ID'))
      ->setDescription(t('The ID of the Note entity.'))
      ->setReadOnly(TRUE);

    $fields['uuid'] = BaseFieldDefinition::create('uuid')
      ->setLabel(t('UUID'))
      ->setDescription(t('The UUID of the Note entity.'))
      ->setReadOnly(TRUE);


    $fields['name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Name'))
      ->setDescription(t('The name of the Note entity.'))
      ->setSettings(array(
        'default_value' => '',
        'max_length' => 50,
        'text_processing' => 0,
      ))
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    // Litteral Note field.
    // ListTextType with a drop down menu widget.
    // The values shown in the menu are 'A', 'B', 'C' and 'D'.
    // In the view the field content is shown as string.
    // In the form the choices are presented as options list.
    $fields['litteral_note'] = BaseFieldDefinition::create('list_string')
      ->setLabel(t('Litteral Note'))
      ->setDescription(t('The litteral Note which evaluate the student.'))
      ->setSettings(array(
        'allowed_values' => array(
          'a' => 'A',
          'b' => 'B',
          'c' => 'C',
          'd' => 'D',
        ),
      ))
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'options_select',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

      // Litteral appreciation field.
      // We set display options for the view as well as the form.
      // Users with correct privileges can change the view and edit configuration.
      $fields['litteral_appreciation'] = BaseFieldDefinition::create('string')
        ->setLabel(t('Litteral appreciation'))
        ->setDescription(t('The Litteral appreciation of the student.'))
        ->setSettings(array(
          'default_value' => '',
          'max_length' => 255,
          'text_processing' => 0,
        ))
        ->setDisplayOptions('view', array(
          'label' => 'above',
          'type' => 'string',
          'weight' => -6,
        ))
        ->setDisplayOptions('form', array(
          'type' => 'string',
          'weight' => -6,
        ))
        ->setDisplayConfigurable('form', TRUE)
        ->setDisplayConfigurable('view', TRUE);

        // Student ID field.
        // Entity reference field, holds the reference to the user object.
        // The view shows the user name field of the user.
        // The form presents a auto complete field for the user name.
        $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
          ->setLabel(t('Student Name'))
          ->setDescription(t('The Name of the student.'))
          ->setSetting('target_type', 'user')
          ->setSetting('handler', 'default')
          ->setDisplayOptions('view', array(
            'label' => 'above',
            'type' => 'entity_reference',
            'weight' => -3,
          ))
          ->setDisplayOptions('form', array(
            'type' => 'entity_reference_autocomplete',
            'settings' => array(
              'match_operator' => 'CONTAINS',
              'size' => 60,
              'autocomplete_type' => 'tags',
              'placeholder' => '',
            ),
            'weight' => -3,
          ))
          ->setDisplayConfigurable('form', TRUE)
          ->setDisplayConfigurable('view', TRUE);

    $fields['langcode'] = BaseFieldDefinition::create('language')
      ->setLabel(t('Language code'))
      ->setDescription(t('The language code of Note entity.'));

    $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.'));

    return $fields;
  } 

Ces 3 propriétés seront configurables dans le formulaire de création et le rendu de l'entité grâce aux options setDisplayConfigurable('form', TRUE) et setDisplayConfigurable('view', TRUE).

Drupal fournit de base les types de champs suivants. En complément, les modules peuvent fournir d'autres types de champs qui peuvent alors être utilisés également.

  • string: un champ text simple.
  • boolean: une valeur booléenne stockée comme un entier.
  • integer: un entier, avec des paramètres minimum et maximum possible pour la validation du champ (disponibles aussi pour les champs de type decimal et float)
  • decimal: une valeur décimal, avec une précision configuration
  • float: un chiffre à virgule flottante
  • language: contient le code du language et les différentes propriétés du language
  • timestamp: une valeur Unix timestamp stockée comme un entier
  • created: un timestamp qui utilise la date actuelle comme valeur par défaut
  • changed: un timestamp qui est automatiquement mis à jour sur la date actuelle si l'entité est sauvé.
  • date: une date stockée selon le format ISO 8601.
  • uri: ce champ contient une uri. Le module link fournit aussi un champ de type lien qui peut inclure un titre de lien et peut pointer vers une adresse/route interne ou externe
  • uuid:  un champ UUID qui génère un nouveau UUID comme valuer par défaut
  • email: Un champ e-mail, avec la validation correspondante et les widgets et formateurs associés
  • entity_reference: un champ entity reference avec un target_id et un champ calculé contenant les propriétés du champ. Le module entity reference fournit les widgets et formateurs quand il est activé.
  • map: peut contenir n'importe quelle quantité de propriétés arbitraires, stockées dans une chaine de texte sérialisée.

Pour activer ces trois nouveaux champs, il faut bien sûr désinstaller / réinstaller notre module s'il était déjà activé, afin de relancer le processus de création de notre entité, et que nos nouveaux champs soient créés au niveau de la table de base de l'entité.

table mysql de notre entité

Notre table contient désormais bien nos nouveaux champs.

configuration de notre formulaire de saisie pour l'entité

Et nous pouvons bien sûr paramétrer notre formulaire de saisie au moyen de l'interface, aussi bien que le rendu de notre entité depuis l'onglet Manage Display ou Gérer l'Affichage si vous avez choisi le français lors de l'installation de Drupal 8.

Nous disposons alors d'une entité sur mesure, dont les champs spécifiques sont stockés dans la même table de la base de données et bien sûr accessibles depuis Views.

formulaire de saisie de notre entité

intégration des champs spécifiques de notre entité dans Views

Il ne restera plus, au niveau de l'intégration de notre entité dans Views, qu'à faire la jointure entre le champ spécifique Student Name et la table des utilisateurs pour pouvoir récupérer toutes leurs informations, si nécessaire.

En guise de conclusion

Le module Console nous permet à la fois de gagner du temps et de disposer d'une base solide et saine pour commencer à implémenter des propriétés ou des logiques métier. Et de par sa nouvelle API et son approche objet, Drupal 8 semble encore plus redoutable que son prédécesseur, pourtant déjà bien armé, et donne encore plus de sens à cette maxime.

Les seules limites de Drupal 8 seront celles de notre imagination.

Au final, cela nous aura pris peut-être un peu plus que 10 secondes, mais pas pour la génération de notre entité brute. Nous sommes d'accord ?

Commentaires

Soumis par Kevin (non vérifié) le 26/10/2014 à 09:42 - Permalien

Salut, j'ai joué un petit peu avec mes entités et principalement leurs propriétés.

J'ai fais ma première entité, ajouté les propriété de type suivant :
- Boolean / Checkbox
- list_string

Sans soucis.

Par contre dans ma seconde entité, j'ai essayé de faire une entity référence à l'image de ton exemple de code et quand je veux créer une instance de mon entité, gros message d'erreur drupal et j'ai ça dans mes logs :

[Sun Oct 26 09:33:55.154583 2014] [:error] [pid 29981] [client 127.0.0.1:48481] Uncaught PHP Exception InvalidArgumentException: "Field user_id is unknown." at /******/www/core/lib/Drupal/Core/Entity/ContentEntityBase.php line 339

Sachant que j'ai aucune référence à user_id dans mon code, voici la définition de mon field :
https://gist.github.com/kgaut/a99e40f5bf4706a8c6c6

et en créant une entity_reference via l'admin entre deux content type j'ai la même erreur, as-tu rencontré ce soucis? est-ce un bug de la beta2 ou un problème d'interface clavier / chaise ?

A+ et encore merci pour ce tuto !

Soumis par Kevin (non vérifié) le 26/10/2014 à 17:13 - Permalien

Super !

Je viens de tester ton patch et cela fonctionne maintenant. Merci beaucoup.

Sur quoi tu te bases pour connaître les différents formateurs possible ? Car à part fouiller dans le code pour des exemples. Je cherche par exemple à faire une liste "Select" plutôt qu'un autocomplete.

Soumis par fabrice le 26/10/2014 à 19:25 - Permalien

Je consulte la documentation de d.o ou sinon son api, et quand je ne trouve pas par reverse engineering (dans les modules du core ou contrib) sans oublier d'aller mettre à jour la documentation de d.o après coup ;-)

Soumis par gir (non vérifié) le 28/10/2014 à 12:44 - Permalien

Est-il possible d'avoir plus de précisions sur la partie views ? Est-ce que le type "note" doit être listé dans la liste déroulante "type" du formulaire de création d'un views ?
Dans mon cas il n'est pas listé.

Soumis par Bigg-Mike (non vérifié) le 10/05/2016 à 11:26 - Permalien

Hi everyone,
deux ans de retard, veuillez m'excuser si je dépoussière le topic...! ^^
J'ai une erreur pour ma part en générant une entité. Je peux créer un field sans soucis, mais quand je veux afficher la liste, j'obtiens cette erreur (en regardant dans les logs):
" Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'drupal-8.deplacement' doesn't exist: SELECT COUNT(*) AS expression FROM (SELECT 1 AS expression FROM {deplacement} base_table) subquery; Array ( ) in Drupal\Core\Entity\EntityListBuilder->getEntityIds() (line 98 of C:\Users\fsanlaville\dev\drupal-8.0.6\core\lib\Drupal\Core\Entity\EntityListBuilder.php)."

Si jamais quelqu'un est déjà tombé sur ce problème et l'a solutionné!
Merci par avance!! :)

J'ai eu la même erreur , tu dois désinstaller ton module d'abord , tu vide ton cache , ensuite tu génère tes entités avec drupal console , tu vide le cache , active ton module et ça marchera :-)

Soumis par Nadir (non vérifié) le 12/05/2016 à 10:22 - Permalien

Salut ,
Merci pour ce super tuto , ça marche nikel sur mon D8.1.1
Mais j'ai une question ;
J'ai généré mon entité avec la console et j'ai ajouté des fields depuis l'interface du drupal avec des références sur user ( FK )
Drupal aprés gère l'incrémentation de création des tables automatiquement dans la base , sauf quand je consulte la structure de mon schéma , je ne vois pas les clés étrangère !
Ma Question est comment drupal 8 pourrait gérer ses jointures sans qu'il voit les FK ?

Soumis par Lasserre Adrie… (non vérifié) le 01/06/2016 à 15:40 - Permalien

Bonjour,

votre article m'a vraiment beaucoup aidé dans mon développement ... jusqu'au moment où j'ai essayé de mettre en forme mon Entity de façon publique (avoir une url publique à la manière de /node/{node_id} ).
Et là, aucune information sur internet (ou très peu), il est vrai que les Entity sont généralement conçues pour êtres des éléments de support (stocker des informations, commandes etc ...) réutilisables dans des modules ou autres.
Mais, pour ceux qui comme moi aimeraient néanmoins proposer une vue publique propre, voici le process. Il manque pas grand chose :

1. Dans le fichier example.routing.yml, ajouter :

example.note.public_view:
path: '/note/{note}'
defaults:
_entity_view: 'note'
requirements:
_permission: 'access content'

Ce qui est important ici, c'est le "{note}" donné en argument du chemin : il faut que le nom soit le même que l'id donné dans les annotations de la classe Entity\Note :
"@ContentEntityType(
id = "note", "
Après, tout le reste c'est à mettre à votre sauce.

C'est ensuite la classe EntityViewBuilder (controlleur de vue par défaut des Entity) qui prend la main, c'est encore une fois défini dans ces annotations @ContentEntityType en préambule de Entity\Note :
"handlers = {
"view_builder" = "Drupal\Core\Entity\EntityViewBuilder","

Vous pouvez créer votre propre surcharge si vous en sentez le besoin .. (cf http://blog.kaliop.com/blog/2016/03/02/drupal-8-controller/)

Ne pas oublier un petit "drush cr" .. et poum.

2. Bon, cela crée une page, mais comment la lier au template (templates/note.htlml.twig + note.page.inc) que nous a généreusement donné la commande 'module generate:entity:content' ? Et là, encore quelques heures de recherches .. En creusant dans le code de EntityViewBuilder on voit qu'il charge la vue de l'Entity en faisant appel à une fonction de theme dont le nom est le même que le nom machine de l'entité. I.e, pour 'note' ce sera .. 'note'.

Il manque donc ce morceau de code dans le fichier example.module, un drush cr, et bam ! ça marche :

"function example_theme() {
return array(
'note' => array(
'render element' => 'elements',
'file' => 'note.page.inc',
'template' => 'note',
),
);
}"

Et voilà.
Bon code à tous.

Soumis par Heyyo (non vérifié) le 12/02/2017 à 15:47 - Permalien

Quelqu'un aurait il reussi a integrer un champs de type Views Bulk operation dans sa vue, afin de modifier en bulk des proprietes, par exemple la langue des entites.

Soumis par Leclerc (non vérifié) le 05/04/2018 à 10:09 - Permalien

"Pour activer ces trois nouveaux champs, il faut bien sûr désinstaller / réinstaller notre module s'il était déjà activé, afin de relancer le processus de création de notre entité, et que nos nouveaux champs soient créés au niveau de la table de base de l'entité."
Pour éviter cette procédure bien longue et pénible lorsque vous construisez votre entité à taton, Drush vient vous sauver avec :
"drush entity:updates"

Soumis par Lechien (non vérifié) le 25/03/2019 à 14:11 - Permalien

Bonjour,

Merci pour cet article qui fait un tour relativement complet sur la création d'entité !

Pour revenir à la question contenue dans l'objet de mon commentaire, savez-vous s'il est préférable de laisser les fichiers générés "intacts" ? et donc de passer par d'autres fichiers/méthodes/hooks...etc pour surcharger/modifier l'entité ?

Ce que je veux dire par là c'est: est-il pertinent d'envisager une mise à jour quelconque du code généré par Drupal console qui obligerait un jour à devoir re-générer l'entité et par conséquent amener à gérer des conflits avec le code modifié par le développeur ?

Encore merci !

Soumis par Condutiarii (non vérifié) le 31/03/2019 à 01:01 - Permalien

Il n'est pas nécessaire de désinstaller/réinstaller le module quand tu ajoutes des propriétés à ton entité, comme ces 3 fameux champs dans ton exemple.

La commande Drupal Console update:entities fait ça très bien.

Ajouter un commentaire