Fil d'Ariane
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 : Classe abstraite : Répertoire d’implémentation : /src/Plugin/Field/FieldType/ Namespace à utiliser : |
|
Nom du hook Drupal 7 |
Équivalent Drupal 8 |
|
Annotation de type @FieldType |
|
|
|
|
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 : Classe abstraite : Répertoire d’implémentation : /src/Plugin/Field/FieldWidget/ Namespace à utiliser : |
|
Nom du hook Drupal 7 |
Equivalent Drupal 8 |
|
Annotation de type @FieldWidget |
|
|
|
|
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 : Classe abstraite : Répertoire d’implémentation : /src/Plugin/Field/FieldFormatter/ Namespace à utiliser : |
|
Nom du hook Drupal 7 |
Equivalent Drupal 8 |
|
Annotation de type @FieldFormatter |
|
|
|
|
|
|
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
Votre commentaire
À 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.
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.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.
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 !