Drupal 10 : Field API / Créer un nouveau formateur, widget ou type de champ

Comment créer un nouveau type de champ pour exploiter le maximum du potentiel de Drupal.

Cet article a été initialement rédigé pour Drupal 8 mais son contenu est toujours d'actualité pour Drupal 9 et Drupal 10.

N'hésitez pas à nous contacter ou à vous inscrire à notre formation «Drupal pour les développeurs et développeuses» pour en savoir plus !

Principes

La création d’un type de champ peut être nécessaire dans Drupal, notamment pour réaliser un champ composé de plusieurs données (comme Link qui propose un titre et une URL). Cela permet de s’affranchir des modules comme Field collection ou Paragraphs pour simplifier le modèle de données ou compléter un type de champ existant proche de nos besoins. Nous allons voir l'implémentation d'un type de champ collectant un ISBN de livre à 10 ou 13 chiffres (2 champs de collecte).

Initiée dans le cœur de Drupal 7 avec l'ingestion de CCK, la Field API a continué d’évoluer. Il était frustrant de ne pas pouvoir appliquer aux propriétés des entités des formateurs ou des widgets. C’est maintenant possible si la déclaration de ces propriétés le permet ou en l’altérant (Cela est nécessaire pour l’entité Node et les champs title, author, created, etc.).

Les propriétés s’appellent des champs de base (Base fields) et les champs “classiques” sont des champs configurables (Configurable fields). Les propriétés n’ont pas totalement disparu : les champs sont composés de propriétés. Exemple : une valeur pour un champ de texte simple (Textfield), une valeur et un format de texte pour un champ de texte long (Textarea).
Dans la plupart des cas, les propriétés seront associées à une colonne dans une table mais pas toujours. Il est possible d’avoir des propriétés qui stockent des données calculées. Dans le cas précédent, le texte rendu dans le format de texte sera une donnée calculée que nous stockerons dans une propriété. Ces propriétés calculées combinées au cache de l’API de rendu permettent d’optimiser les performances. Pour voir comment implémenter ces données calculées, référez-vous à notre article sur les Computed Fields ou à la documentation.

Les types de champs, formateurs et widgets sont des Plugins. Si vous souhaitez implémenter l’un de ces trois type de Plugin il suffit d’implémenter l’interface associée ou, si vous ne souhaitez pas réinventer la roue, étendre la classe annotée de base de chaque type :

TYPE DE PLUGIN

ANNOTATION

INTERFACE

CLASSE

Type de champ

@FieldType

FieldItemInterface

FieldItemBase

Widget

@FieldWidget

WidgetInterface

WidgetBase

Formateur

@FieldFormatter

FormatterInterface

FormatterBase


Le chemin PSR-4 de votre classe prend la forme suivante :  src/Plugin/Field/Field<Type|Widget|Formatter>/<nomPlugin>.php

À noter également que les champs sont maintenant stockés par type d’entité. Il devient donc possible d’utiliser le même nom de champ à plusieurs endroits.

Pour les développeurs habitués à développer avec les champs depuis Drupal 7, un changement sémantique est intervenu avec Drupal 8. La notion de “champ” (Field) définissant la structure des données est maintenant appelé FieldStorage alors que la notion “d’instance de champ” identifiant la configuration d’un champ associé à une entité s’appelle désormais Field.

Exemple

Dans Drupal un champ est composé de 3 parties. Une principale, le type de champ (FieldType) qui est la définition technique du champ, et deux parties d’interface ; à savoir : le widget (FieldWidget) utilisé pendant l’édition d’un contenu et le formateur (FieldFormatter) qui s’occupe du rendu du champ lors de son affichage.

Ces deux derniers éléments peuvent être créés indépendamment du FieldType, ce qui permet de proposer des FieldWidget ou des FieldFormatter pour n’importe quel FieldType.

Chacune de ces 3 parties est gérée à l’aide de Plugins. Voici pour chacune les informations nécessaires à leur implémentation ainsi qu’un aperçu des méthodes qui remplissent les fonctions d’anciens hooks sous Drupal 7.

Le stockage des données

FieldType 

Interface : Drupal\Core\Field\FieldItemInterface

Classe abstraite : Drupal\Core\Field\FieldItemBase

Répertoire d’implémentation : /src/Plugin/Field/FieldType/

Namespace à utiliser : Drupal\<module>\Plugin\Field\FieldType

Nom du hook Drupal 7

Équivalent Drupal 8

hook_field_info()

Annotation de type @FieldType

hook_field_schema()

FieldItemInterface::schema()

hook_field_is_empty()

ComplexDataInterface::isEmpty()

L’Annotation de ce Plugin est assez simple, l’identifiant machine, un label, une description et les valeurs par défaut du widget et du formateur utilisés pour ce champ.

#/src/Plugin/Field/FieldType/IsbnItem.php

/**
* Plugin implementation of the 'isbn' field type.
*
* @FieldType(
*   id = "isbn",
*   label = @Translation("Isbn"),
*   description = @Translation("Stores a ISBN string in various format."),
*   default_widget = "isbn_default",
*   default_formatter = "isbn",
* )
*/

La création d’un type de champ passe par la définition du modèle de données de ce champ. Pour cela il faut implémenter les méthodes schema() et propertyDefinitions(). Comme pour Drupal 7, avec le hook_field_schema() il s’agit de décrire la table SQL qui va recevoir les données.

Dans notre cas nous aurons 2 valeurs de l’ISBN à stocker.

#/src/Plugin/Field/FieldType/IsbnItem.php
public static function schema(FieldStorageDefinitionInterface $field_definition) {
 return array(
   'columns' => array(
     'isbn_13' => array(
       'description' => 'The isbn number with 13 digits.',
       'type' => 'varchar',
       'length' => 13,
     ),
     'isbn_10' => array(
       'description' => 'The isbn number with 10 digits.',
       'type' => 'varchar',
       'length' => 10,
     ),
   ),
 );
}

La méthode propertyDefinitions() quant à elle permet une description au niveau de Drupal et propose plus d’informations. La description des propriétés se fait grâce à la Typed Data API qui permet d’interagir avec les données et leurs meta-données. Exemple : donner un nom plus compréhensible par un humain avec setLabel(), rendre un champ obligatoire avec setRequired(), définir des contraintes de validation avec addConstraint()...

#/src/Plugin/Field/FieldType/IsbnItem.php
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
  $properties['isbn_13'] = DataDefinition::create('string')->setLabel(t('ISBN-13'));
  $properties['isbn_10'] = DataDefinition::create('string')->setLabel(t('ISBN-10'));
  return $properties;
}

Si l’un veut créer un champ composé, deux autres méthodes sont particulièrement importantes. isEmpty() permet à Drupal de savoir si votre champ doit être considéré comme vide pour afficher ou non le champ par exemple. Dans notre cas, on va considérer que c’est la valeur de la propriété ‘isbn_13’ qui va déterminer cela.

#/src/Plugin/Field/FieldType/IsbnItem.php
public function isEmpty() {
 $value = $this->get('isbn_13')->getValue();
 return empty($value);
}

La deuxième est mainPropertyName() qui permet de définir le nom de la propriété principale. La plupart des champs de base utilisent ‘value’ mais cela devient vite gênant quand on construit des champs complexes. Il est donc essentiel de fournir cette information aux autres modules pour qu’ils puissent utiliser au mieux notre champ.

#/src/Plugin/Field/FieldType/IsbnItem.php
public static function mainPropertyName() {
 return 'isbn_13';
}

Bien sûr il existe encore de multiples méthodes, notamment les fieldSettingsForm(), preSave(), delete() et autres pour agir à différents moments de la vie de nos données de champ mais je vous laisse découvrir cela en regardant l’interface Drupal\Core\Field\FieldItemInterface.

On notera la présence de generateSampleValue() permettant de fournir un jeu de données basiques jouant le rôle de données de substitution lors de la génération de contenus fictifs. (Avec Devel generate par exemple).

Le widget du champ

FieldWidget

Interface : Drupal\Core\Field\WidgetInterface

Classe abstraite : Drupal\Core\Field\WidgetBase

Répertoire d’implémentation : /src/Plugin/Field/FieldWidget/

Namespace à utiliser : Drupal\<module>\Plugin\Field\FieldWidget

Nom du hook Drupal 7

Equivalent Drupal 8

hook_field_widget_info()

Annotation de type @FieldWidget

hook_field_widget_form()

WidgetInterface::formElement()

hook_field_widget_error()

WidgetInterface::errorElement()

Encore une fois, on utilisera un Plugin pour créer le widget de notre champ. Nous allons donc le définir à l’aide d’une Annotation.

#/src/Plugin/Field/FieldWidget/IsbnWidget.php

/**
 * Plugin implementation of the 'isbn' widget.
 *
 * @FieldWidget(
 *   id = "isbn_default",
 *   label = @Translation("ISBN"),
 *   field_types = {
 *     "isbn"
 *   }
 * )
 */

Ensuite, nous allons définir le formulaire qui sera utilisé dans l’interface pour réaliser la saisie des valeurs du champs dans la méthode formElement().

#/src/Plugin/Field/FieldWidget/IsbnWidget.php

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element['isbn_13'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('ISBN-13'),
      '#placeholder' => $this->getSetting('placeholder_isbn_13'),
      '#default_value' => isset($items[$delta]->isbn_13) ? $items[$delta]->isbn_13 : NULL,
      '#required' => $element['#required'],
    );

    $element['isbn_10'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('ISBN-10'),
      '#placeholder' => $this->getSetting('placeholder_isbn_10'),
      '#default_value' => isset($items[$delta]->isbn_10) ? $items[$delta]->isbn_10 : NULL,
    );

    return $element;
  }

On remarquera l’utilisation de getSettings() qui permet de récupérer de la configuration qui pourrait être définie via settingsForm() et configurable dans l’interface de gestion de l’affichage du formulaire.

Le formateur du champ

FieldFormatter

Interface : Drupal\Core\Field\FormatterInterface

Classe abstraite : Drupal\Core\Field\FormatterBase

Répertoire d’implémentation : /src/Plugin/Field/FieldFormatter/

Namespace à utiliser : Drupal\<module>\Plugin\Field\FieldFormatter

Nom du hook Drupal 7

Equivalent Drupal 8

hook_field_formatter_info()

Annotation de type @FieldFormatter

hook_field_formatter_view()

FormatterInterface::viewElements()

hook_field_formatter_settings_form()

FormatterInterface::settingsForm()

hook_field_formatter_settings_summary()

FormatterInterface::settingsSummary()

Pour le formateur d’un champ, le travail est le même, cela débute par l’implémentation d’un Plugin avec une Annotation @FieldFormatter. Il faut ensuite implémenter viewElements() pour définir le rendu des valeurs. Enfin settingsForm() et settingsSummary() permettent de définir le formulaire des paramètres du champ et le résumé de leur valeur utilisés dans l’interface de gestion des View modes..

Une version détaillée de cette partie est visible dans notre article sur la création d'un formateur de champs.

Validation des données

Drupal introduit un concept de validateurs de contraintes issu de Symfony permettant de contrôler les valeurs d’un fieldType à la sauvegarde. On pourrait rajouter par exemple la ligne ->addConstraint('Length', array('max' => 13, min => 13)) sur les propriétés définies dans propertyDefinitions() pour que cette validation soit faite automatiquement. Il en existe de plusieurs type (unicité, plage, bundle, etc) et il est possible de les étendre car ce sont des plugins de type @Constraint, cela sera vu dans un autre chapitre.

De manière plus classique, il est toujours possible de faire des validations au niveau du formulaire du widget en utilisant la Form API. La validation d’un élément du formulaire utilise toujours #element_validate, par contre on passe maintenant un tableau avec la classe utilisée et la méthode de la classe, plutôt qu’un nom de fonction.

#/src/Plugin/Field/FieldWidget/IsbnWidget.php

$element['isbn_13'] = array(
 '#type' => 'textfield',
 '#title' => $this->t('ISBN-13'),
 '#placeholder' => $this->getSetting('placeholder_isbn_13'),
 '#default_value' => $default_isbn_value,
 '#required' => $element['#required'],
 '#element_validate' => array(array($this, 'validateIsbnElement')),
);

Schéma et Configuration du champ

La plupart des Plugins implémentés durant cet exercice peuvent s’enrichir de configurations. Dans ce cas, elles sont stockées à l’aide d’entités de configuration et il faut déclarer le schéma de ces entités pour qu’elles puissent être exportées par le gestionnaire de configuration, profiter de la traduction et permettre le typage automatique des données.

La déclaration de ces schémas se fait dans le fichier /config/schema/isbn.schema.yml comme cela a été vue dans le chapitre sur la configuration (Configuration : fondements)

#/config/schema/isbn.schema.yml

field.widget.settings.isbn_default:
 type: mapping
 label: 'Isbn format settings'
 mapping:
   placeholder_isbn_10:
     type: string
     label: 'Placeholder for ISBN 10'
   placeholder_isbn_13:
     type: string
     label: 'Placeholder for ISBN 13'

Commentaires

Jerome Mardi 24 mai 2016 - 06:47
Merci pour votre tuto. Pour ma part, j'ai créé un petit champ sur mesure qui fonctionne correctement avec des datas type varchar, mais je bloque sur l'intégration d'un champ file_managed (pour charger une image). L'erreur reportée est : "Uncaught PHP Exception Drupal\\Core\\Entity\\EntityStorageException: "Placeholders must have a trailing [] if they are to be expanded with an array of values." at /core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php line 756"... si des fois ça vous parle ;-)
En réponse à par Jerome
DuaelFr Mercredi 25 mai 2016 - 16:01

Salut Jérome,

Sans contexte c'est pas évident de te répondre. À vue de nez je dirais que tu ne fournis pas un tableau en valeur de ton champ alors qu'il en attend un. J'imagine qu'à la façon de D7, un simple (array) $file devrait suffire.

Modeste Mercredi 7 août 2019 - 05:58

Bonjour,

 

Merci beaucoup pour votre tuto. Il m'a permis de créer mon formulairede contact personnalisé.

 

Cependant j'ai un problème. En effet, j'ai mis en place une validation Ajax pour les donnés et cela marche très bien, mais seulement quand je suis connecté dans admin. Quand je me déconnecte, elle ne marche plus. Aussi j'ai créé un block pour le formulaire (pour l'afficher dans mon footer) et il s'affiche bien, mais là aussi la validation Ajax ne marche pas, peu importe si je suis connecté ou non. 

 

Avez-vous une idée de ce qui peut causer cela s'il vous plait ? Merci de me répondre.

 

 

En réponse à par Modeste
DuaelFr Vendredi 9 août 2019 - 10:42

Bonjour Modeste,

Malheureusement, c'est très difficile de répondre à votre question sans avoir plus de détails et les commentaires de cet article ne sont pas l'endroit idéal pour échanger. Je vous invite à rejoindre la communauté française sur Slack en suivant les instructions proposées par l'association Drupal France et Francophonie. Vous y trouverez un endroit où exposer votre problème et discuter avec des personnes qui pourront peut-être vous aider plus efficacement.

Bonne journée !

Votre commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.
Votre adresse servira à afficher un Gravatar et à vous notifier des réponses. Votre commentaire sera anonymisé si ce billet est dépublié pendant plus de 3 mois.
Pour lutter contre le spam notre système enregistre votre adresse IP et votre adresse e-mail si vous la partagez.
Nous vous invitons à consulter notre politique de confidentialité pour comprendre les traitements faits de ces données et comment les rectifier.

À propos de Julien

Co-fondateur - Scrum master & Expert technique

Utilisateur de Drupal depuis 2008, j’ai fait mes armes comme développeur chez Commerce Guys puis me suis mis à encadrer les nouveaux arrivants avant de donner des formations, participer aux avant ventes et accompagner les équipes au passage à Scrum.

Je suis impliqué dans la communauté française de Drupal depuis 2009, j’ai été tour à tour président puis vice-président de l’association Drupal France et francophonie entre 2011 et 2013.