Dans la série d'articles sur la Création d'un module avec Drupal 8, le dernier concernait ConfigEntityBase, le système d'entité pour la sauvegarde de configuration. Nous avons créé une entité héritant de ce type et nous y avons ajouté les variables nécessaires pour le stockage de la configuration globale de notre module Internal Link. Nous allons maintenant voir comme nous allons procéder pour proposer à l'utilisateur un champ de formulaire sur lequel il pourra intégrer le paramétrage du module.
Introduction
Drupal 8 intègre une annotation de type @FormElement permettant de définir un élément de formulaire, héritant de la classe FormElement, elle même héritant de RenderElement. FormElement correspond a un élément de rendu pour les formulaires HTML. Tout l'intérêt réside dans le fait que nous puissions créer des éléments de formulaire dédiés à des données particulières, et que nous pouvons réutiliser dans n'importe quel formulaire. Avant Drupal 8, nous étions cantonnés à une série d'éléments de formulaire que nous ciblions en utilisant la propriété #type: select, textarea, textfield, checkbox, radio, etc... Aujourd'hui, nous pouvons créer notre propre élément de formulaire (par exemple un élément complexe contenant plusieurs champs) et utiliser la propriété #type pour le déclarer. Nous passons ensuite par d'autres propriétés pour lui transmettre des informations, et nous pouvons récupérer les valeurs du ou des champs dans le $form_state au niveau de la validation ou de la soumission du formulaire.
Définition de notre élément de formulaire
Dans notre cas, nous désirons avoir un élément de formulaire qui va proposer une série de champs, chacun correspondant à une valeur de paramétrage de notre configuration.
Implémentation
Nous allons commencer par créer un répertoire src/Element pour y ajouter un fichier InternalLinkConfiguration.php.
Base
Nous allons ensuite y rajouter le squelette de notre classe:
/** * @file * Contains \Drupal\internal_link\Element\InternalLinkConfiguration */ namespace Drupal\internal_link\Element; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element\FormElement; /** * Provide internal link element configuration * * @FormElement("internal_link_configuration") */ class InternalLinkConfiguration extends FormElement { /** * {@inheritDoc} * @see \Drupal\Core\Render\Element\ElementInterface::getInfo() */ public function getInfo() { $class = get_class($this); return [ '#input' => TRUE, '#tree' => TRUE, '#process' => [ [$class, 'processConfiguration'], ], ]; } /** * Process handler for the internal_link_configuration form element. * * @param array $element * Processed element. * @param FormStateInterface $form_state * Current form state object. * @param array $form * Current form object. * * @return array * The final form element. */ public static function processConfiguration(&$element, FormStateInterface $form_state, &$form) { return $element; } }
L'annotation est basique:
/** * @FormElement("internal_link_configuration") */
Elle indique simplement le #type que nous utiliserons lors de son intégration dans un formulaire.
Nous trouvons ensuite la déclaration de classe, puis l'implémentation de la méthode getInfo:
public function getInfo() { $class = get_class($this); return [ '#input' => TRUE, '#tree' => TRUE, '#process' => [ [$class, 'processConfiguration'], ], ]; }
Cette implémentation de méthode permet de retourner les propriétés de l'élément. La plupart des propriétés usuelles peuvent être passées (voir les commentaires de la classe FormElement pour une liste des propriétés standard). Nous indiquons que le résultat des valeurs dans $form_state devra être de type hiérarchique (#tree = TRUE) et nous indiquons également quelle est la fonction de "processing" qui va s'occuper de construire l'élément.
Nous trouvons ensuite un squelette de méthode correspondant au callback #process, dont nous allons voir en détail l'implémentation.
Méthode de construction du champ
La méthode de construction est la plus importante. C'est elle qui va définir le contenu de notre champ. Dans notre cas, nous devons proposer toute une série de champs qui vont chacun correspondre à une valeur de paramétrage:
public static function processConfiguration(&$element, FormStateInterface $form_state, &$form) { $options = isset($element['#options']) ? $element['#options'] : [ 'fields' => [], ]; unset($element['#options']); $html_name = $element['#name']; /** @var InternalLinkSettings $config */ $config = $element['#default_value']; if (!isset($config)) { $config = InternalLinkSettings::getDefaultConfiguration(); } $element['automatic_enabled'] = [ '#type' => 'checkbox', '#title' => t('Enable automatic mode'), '#description' => t('Enable the internal link automatic mode to automatically replace words by matching links if no manual links are set.'), '#default_value' => $config->isAutomaticEnabled(), ]; $element['manual_enabled'] = [ '#type' => 'checkbox', '#title' => t('Enable manual mode'), '#description' => t('Enable the internal link manual mode to let author with correct permissions choosing between manual internal links.'), '#default_value' => $config->isManualEnabled(), ]; $element['fields'] = [ '#type' => 'checkboxes', '#title' => t('Available fields'), '#description' => t('Choose the field(s) into you want replace words by links.'), '#options' => $options['fields'], '#default_value' => $config->getFields(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], 'required' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $element['wrap_html_tag'] = [ '#type' => 'textfield', '#title' => t('Wrap HTML tag'), '#description' => t('Enter HTML tag which will be used to wrap internal link. Be careful and enter only tag name (e.g. h1).'), '#size' => 10, '#default_value' => $config->getWrapHTMLTag(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $element['disallowed_html_tags'] = [ '#type' => 'textfield', '#title' => t('Disallowed HTML tags'), '#description' => t('A list of HTML tags that will be ignored. Never enter here tags that are not text. E.g. @tags.', array('@tags' => '<img />')), '#default_value' => $config->getDisallowedHTMLTags(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $element['highlight_words'] = [ '#type' => 'checkbox', '#title' => t('Highlight words'), '#description' => t('Highlight words instead of replace it to links.'), '#default_value' => $config->getHightlightWords(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $element['word_boundary'] = [ '#type' => 'checkbox', '#title' => t('Word boundary'), '#description' => t('If enabled all words will be converted, even those that are not wrapped by spaces.'), '#default_value' => $config->getWordBoundary(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; $numeric_options = array_combine($list, $list); $element['links_to_process'] = [ '#type' => 'select', '#title' => t('Links to process'), '#description' => t('Choose the maximum number of links to process.'), '#options' => $numeric_options, '#default_value' => $config->getLinksToProcess(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; $element['maximum_application'] = [ '#type' => 'select', '#title' => t('Link maximum application'), '#description' => t('Choose the number of time a link must be applied in one text fragment.'), '#options' => $numeric_options, '#default_value' => $config->getMaximumApplication(), '#states' => [ 'visible' => [ [':input[name="'.$html_name.'[automatic_enabled]"]' => ['checked' => TRUE]], [':input[name="'.$html_name.'[manual_enabled]"]' => ['checked' => TRUE]], ], ], ]; return $element; }
Décomposons la méthode pour mieux comprendre comment nous allons pouvoir déclarer notre champ dans un formulaire.
Les options
Nous trouvons d'abord un bout de code qui récupère ou construit un tableau d'options puis le supprime de l'élément:
Nous pouvons voir que ce tableau d'options est sensé contenir une clé 'fields' qui stocke un tableau de valeurs.
Les informations sur l'entité en cours
Nous récupérons ensuite, via le nom HTML de notre élément, ce qui va nous permettre d'affecter des états (#states) selon certains paramètres:
$html_name = $element['#name'];
La configuration actuelle et par défaut
Viennent ensuite quelques lignes qui vont nous permettre de récupérer le paramétrage de la configuration:
/** @var InternalLinkSettings $config */ $config = $element['#default_value']; if (!isset($config)) { $config = InternalLinkSettings::getDefaultConfiguration(); }
La configuration actuelle est stockée dans la propriété #default_value. Si elle n'est pas valorisée, nous utilisons la méthode statique InternalLinkSettings::getDefaultConfiguration pour récupérer une entité temporaire qui contiendra les valeurs de configuration par défaut pour attribuer les valeurs aux différents champs.
Les champs affichés
Enfin, tout le reste de la méthode spécifie les champs qui vont être affichés afin de proposer un élément de formulaire prêt à être utilisé pour le paramétrage de la configuration globale.
Conclusion
Vous constaterez que la logique est relativement simple. Peut-être qu'elle n'est pas vraiment compréhensible pour le moment, car nous avons un élément de formulaire mais nous ne savons pas encore comment il va être utilisé. ça sera le sujet d'un prochain article, mais auparavant, nous devrons encore faire une incursion dans l'API Field de Drupal 8 pour créer un champ que nous pourrons utiliser dans des bundles.