Aller au contenu principal
Julien Dubois

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

Ce billet a été produit dans le contexte d'Happyculture, il a pu être écrit en collaboration.

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


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

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&lt;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&lt;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&lt;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'

Pour rebondir