Créer un bloc configurable dans Drupal

Drupal 8

Créer un bloc configurable dans Drupal

Soumis par Antoine le mar 24/03/2020 - 16:02

Un bloc configurable est un bloc qui propose un formulaire lorsqu'on le place dans une région.

Les étapes

  1. Créer un fichier src/Plugin/Block/nombloc.php à la racine du module qui déclare le bloc à l’aide des annotations.
  2. Créer dans ce fichier une classe qui étend la classe BlocBase et qui implémente l’interface BlockPluginInterface.
  3. Dans cette classe, créer une méthode build() pour définir un thème et valoriser des variables qui seront utilisés à l’affichage du bloc.
  4. Créer une méthode blockForm() qui va générer le formulaire de configuration affiché lorsque l’utilisateur placera le bloc
  5. Créer une méthode blockSubmit() pour réaliser des actions lorsque l’utilisateur valide le formulaire
  6.  Déclarer le thème et indiquer le gabarit à utiliser dans la fonction « nomtheme_theme » du fichier « nomtheme.module »
  7. Créer le gabarit.

Exemple : un bloc qui permet de choisir les outils de partage affichés

Dans cet exemple, nous allons créer un bloc qui pourra afficher un sous-titre ainsi que des boutons de partage Facebook et Twitter. Lorsqu’il placera le bloc, l’utilisateur pourra choisir le contenu du sous-titre et les boutons qu’il veut afficher. Le bloc s’appellera blockboutonpartagechoix.

Dans l'exemple, le lien Twitter est généré en php avec un render array. Par contre le lien Facebook est codé en html dans le fichier twig. Le but était de présenter les deux méthodes. 

Le fichier src/Plugin/Block/nombloc.php

On commence par créer le fichier src/Plugin/Block/ blockboutonpartage.php à la racine de notre module. Vous trouverez ci-dessous le code complet puis des explications pour chaque partie du code

<?php

namespace Drupal\share_configurable\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

/**
 * Provides a Block.to share with Facebook
 *
 * @Block(
 *   id = "blockboutonpartagechoix",
 *   admin_label = @Translation("Block to share with Facebook or Tweeter"),
 *   category = @Translation("Social network"),
 * )
 */
class blockboutonpartagechoix extends BlockBase implements BlockPluginInterface {

  /**
   * {@inheritdoc}
   * Cette fonction génére le formulaire.
   */
  public function build() {

    //Génére le sous titre du bloc
    if ($this->configuration['asksubtitle']=='yes' && strlen($this->configuration['subtitle'])>0){
      $build['subtitle']=[
        '#type' => 'html_tag',
        '#tag' => 'h2',
        '#value' => $this->configuration['subtitle'],
      ];
    }

    //Création d'un lien aec un render array
    global $base_url;
    $url = Url::fromUri('https://twitter.com/share?url='.$base_url.Url::fromRoute('<current>')->toString());
    $pathmodule=drupal_get_path('module', 'share_configurable');
    $uritweeter=$pathmodule.'/src/img/tweeter.png';
    if ($this->configuration['network']['tweeter']=='tweeter'){
      $build['link_tweeter'] = [
        '#title' => [
          '#type' => 'html_tag',
          '#tag' => 'img',
          '#attributes'=>[
            'src' => $uritweeter,
            'height' => '30',
            'width' => '30',
          ]
        ],
        '#type' => 'link',
        '#url' => $url
      ];
    }



    $variable['build']=$build;
    $variable['config']=$this->configuration;

    return [
      '#theme' => 'blockboutonpartagechoix',
      '#title' => t('Share this page !'),
      '#description' => t('a bloc to share content with facebook or tweeter'),
      '#variable' => $variable,

    ];
  }

  /**
   * {@inheritdoc}
   * Cette fonction génére le formulaire affiché lorsqu'on ajoute le bloc à une région
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);

    $form['asksubtitle'] = [
      '#type' => 'radios',
      '#title' => $this->t('Do you want a subtitle?'),
      '#default_value' => isset($this->configuration['asksubtitle']) ? $this->configuration['asksubtitle'] : "no",
      '#options' => [
        "yes"=>$this->t('Yes'),
        "no"=>$this->t('No')
      ],
      '#required'=>true,
      '#attributes' => [
        'data-n' => 'asksubtitle',
      ],
    ];

    $form['subtitle'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Add subtitle above the images'),
      '#description' => $this->t('Something to say ?'),
      '#default_value' => isset($this->configuration['subtitle']) ? $this->configuration['subtitle'] : '',
      '#states' => [
        //show this textfield only if the radio 'yes' is selected above
        'visible' => [
          ':input[data-n="asksubtitle"]' => ['value' => "yes"],
        ],
      ],
    ];


    # the options to display in our checkboxes
    $toppings = array(
      'tweeter' => t('Tweeter'),
      'facebook' => t('Facebook'),
    );

    $defaultvalue=[];
    foreach ($toppings as $key=>$topping){
      if (isset($this->configuration['network'][$key]) && $this->configuration['network'][$key]!==0){
        $defaultvalue[$key]=$key;
      }
    }


# the drupal checkboxes form field definition
    $form['network'] = array(
      '#title' => t('Network choice'),
      '#type' => 'checkboxes',
      '#description' => t('Select the network share button'),
      '#options' => $toppings,
      '#default_value' => $defaultvalue,
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   * Cette fonction est lancé lors de la soumission du formulaire.
   * Elle renregistre dans la configuration les valeurs saisie par le formulaire
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    parent::blockSubmit($form, $form_state);

    $values = $form_state->getValues();

    $this->configuration['subtitle'] = $values['subtitle'];
    $this->configuration['network'] = $values['network'];
    $this->configuration['asksubtitle'] = $values['asksubtitle'];

    foreach ($form_state->getValues() as $key => $value) {
      if (!is_array($value)){
        \Drupal::messenger()->addMessage($key . ': ' . $value);
      }else{
        foreach ($value as $k=>$v){
          \Drupal::messenger()->addMessage($k . ': ' . $v);
        }
      }

    }
  }

  /**
   * {@inheritdoc}
   * Cette fonction est lancé à la soumission du formulaire. Elle contient des règles à respecter pour que la fonction submit soit lancée
   */
  public function blockValidate($form, FormStateInterface $form_state) {
    $subtitle = $form_state->getValue('subtitle');
    $network = $form_state->getValue('network');
    $asksubtitle = $form_state->getValue('asksubtitle');

    if ($asksubtitle=="yes" && strlen($subtitle)==0) {
      $form_state->setErrorByName('subtitle', t('Subtitle must contain at least one character'));
    }

    if ($network['tweeter']=="0" && $network['facebook']=="0"){
      $form_state->setErrorByName('network', t('Chosse at least one network'));
    }
  }
}

  1.  

En-tête du fichier

Dans ce fichier on commence par relier ce fichier au namespace Block et on déclare l’utilisation des classes BlockBase BlockPluginInterface et FormStateInterface :

namespace Drupal\premiermo\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url; //Permet d'avoir des fonctions pour générer des images

Ensuite on crée une classe qui doit porter le même nom que notre fichier. Cette classe étend la classe BlockBase et implémente la classe BlockPluginInterface. Attention les commentaires au-dessus de la classe sont des annotations. Elles permettent de définir les caractéristiques de notre Plugin. C’est un bloc qui à un id, un nom pour les utilisateurs et une catégorie.

/**
 * Provides a Block.to share with Facebook
 *
 * @Block(
 *   id = "blockboutonpartagechoix",
 *   admin_label = @Translation("Block to share with Facebook or Tweeter"),
 *   category = @Translation("Social network"),
 * )
 */

class blockboutonpartagechoix extends BlockBase implements BlockPluginInterface {
}

    1.  
  1.  

La méthode blockForm($form, FormStateInterface $form_state)

 Cette méthode va retourner un tableau associatif qui comprend les informations pour générer le formulaire de configuration du bloc lors de son rendu. Chaque champ du formulaire sera décrit par un tableau associatif.

On commence par définir la méthode

  public function blockForm($form, FormStateInterface $form_state) {
      }

Ensuite on crée le tableau en appelant la fonction blockForm de la classe Blockbase Si l’on va voir ce que fait cette fonction, on remarque qu’elle ne retourne qu’un tableau vide. Elle pourrait servir à modifier tous les formulaires des blocs.

$form = parent::blockForm($form, $form_state);

 Ensuite on doit insérer dans le tableau $form des tableaux associatifs qui décrivent chaque champs d’un formulaire. Dans notre exemple, nous allons créer 3 champs :

  • Un couple de bouton radio pour choisir si l’on veut afficher ou non un sous-titre
  • Un champ texte pour saisir le sous-titre. Ce champ n’apparaitra que si l’utilisateur a coché le bouton « oui » du champ précédent.
  • Des cases à cocher pour choisir les boutons de partage que l’on souhaite afficher

Les boutons radios :

La clé #type permet de définir le type de champs.

La clé #title permet d’indiquer le label du champ.

La clé #default_value permet de pré-coché un des boutons. Dans notre cas, on vérifie si une réponse à déjà été enregistrée. Si oui, on coche le bouton correspondant, sinon on pré-coche le bouton « no ».

La clé #options est un tableau associatif dont les clés correspondent aux attributs values des boutons et les valeurs à leurs labels.

La clé #required permet d’imposer qu’un des boutons soit coché.

La clé #attributes est un tableau associatif dont les clés sont des attributs qui seront ajoutés aux boutons. La valeur de chaque clé, correspond à la valeur de chaque attribut. Ici on rajoute un attribut data-n qui permettra d’appeler facilement le champ. J’ai essayé avec l’attribut name mais je n’arrivais plus à récupérer les valeurs des boutons cochés.

$form['asksubtitle'] = [
  '#type' => 'radios',
  '#title' => $this->t('Do you want a subtitle?'),
  '#default_value' => isset($this->configuration['asksubtitle']) ? $this->configuration['asksubtitle'] : "no",
  '#options' => [
    "yes"=>$this->t('Yes'),
    "no"=>$this->t('No')
  ],
  '#required'=>true,
  '#attributes' => [
    'data-n' => 'asksubtitle',
  ],
];

Le champ texte :

La clé #type permet de définir le type de champs

La clé #title permet d’indiquer le label du champ

La clé #default_value permet de pré-remplir le champ si une valeur a déjà été enregistrée.

La clé #states permet d’afficher le champ uniquement si le bouton radio « yes » a été cochée. Vous trouverez d’autres exemples d’état à cette adresse de la doc officielle :

https://www.drupal.org/docs/8/api/form-api/conditional-form-fields

$form['subtitle'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Add subtitle above the images'),
  '#description' => $this->t('Something to say ?'),
  '#default_value' => isset($this->configuration['subtitle']) ? $this->configuration['subtitle'] : '',
  '#states' => [
    //show this textfield only if the radio 'yes' is selected above
    'visible' => [
      ':input[data-n="asksubtitle"]' => ['value' => "yes"],
    ],
  ],
];

Les cases à cocher pour choisir les boutons qui s’affichent :

On commence par créer un tableau associatif pour créer les différentes cases. La clé correspond à l’attribut « value » et la valeur au label.

$options = array(
  'tweeter' => t('Tweeter'),
  'facebook' => t('Facebook'),
);

Ensuite on génère un tableau associatif pour définir les valeurs par défaut, c’est-à-dire les cases qui doivent être pré cochées lorsque le formulaire a déjà été rempli une fois.

Dans la méthode submit, les valeurs saisies par l’utilisateurs sont enregistrées en base et accessible grâce à l’attribut « configuration ». L’attribut « configuration » est un tableau associatif qui contient une clé pour chaque clé du tableau $form. Comme notre champs case à coché est enregistré dans $form['network]'], alors la valeur de chaque case est accessible dans le tableau $this->configuration['network'].

Ici on boucle sur chaque case à coché pour vérifier si sa valeur a déjà été enregistrée pour autre chose que 0. Si c’est le cas, on rajoute cette case dans le tableau associatif defaultvalue

$defaultvalue=[];
foreach ($options as $key=>$option){
  if (isset($this->configuration['network'][$key]) && $this->configuration['network'][$key]!==0){
    $defaultvalue[$key]=$key;
  }
}

Enfin, on crée le tableau associatif du champ

# the drupal checkboxes form field definition
    $form['network'] = array(
      '#title' => t('Network choice'),
      '#type' => 'checkboxes',
      '#description' => t('Select the network share button'),
      '#options' => $options,
      '#default_value' => $defaultvalue,
    );

      1.  

La méthode blockSubmit($form, FormStateInterface $form_state)

Cette méthode gère les actions à mener quand l’utilisateur valide le bloc.

Ici, on commence par définir la méthode avec les arguments $form qui correspond au tableau associatif compet du formulaire et $form_state qui correspond au formulaire rempli.

lance la méthode blocksubmit() de la classe BlockBase()

public function blockSubmit($form, FormStateInterface $form_state) {
  parent::blockSubmit($form, $form_state);
  }

Ensuite on récupère les valeurs des champs du formulaire grâce à la méthode getValues

$values = $form_state->getValues();

Il reste plus qu’à valoriser l’attribut configuration qui est automatiquement stocké en base de données

$this->configuration['subtitle'] = $values['subtitle'];
$this->configuration['network'] = $values['network'];
$this->configuration['asksubtitle'] = $values['asksubtitle'];

Il est possible d’afficher des messages grâce à la méthode \Drupal::messenger()->addMessage()

Par exemple, dans une optique de débogage, on peut afficher l’ensemble des valeurs des champs du formulaire avec le code suivant :

foreach ($form_state->getValues() as $key => $value) {
  if (!is_array($value)){
    \Drupal::messenger()->addMessage($key . ': ' . $value);
  }else{
    foreach ($value as $k=>$v){
      \Drupal::messenger()->addMessage($k . ': ' . $v);
    }
  }
}
      1.  

La méthode blockValidate($form, FormStateInterface $form_state)

La méthode blockvalidate() permet de mettre en place des conditions de validation du formulaire. La méthode blockSubmit() ne ser apas exécutée si ces conditions ne sont pas respectées. Dans notre cas, on veut empêcher l’enregistrement du formulaire si

  • Le soustitre est vide alors que l’utilisateur à cocher la case pour en afficher un
  • Il n’y a aucun réseau social de coché.

 On commence par récupérer les valeurs du formulaire

public function blockValidate($form, FormStateInterface $form_state) {
  $subtitle = $form_state->getValue('subtitle');
  $network = $form_state->getValue('network');
  $asksubtitle = $form_state->getValue('asksubtitle');
    }
}

Puis si des conditions sont réunies, on utilise la méthode setErrorByName(le champ non valide, le message à afficher)

if ($asksubtitle=="yes" && strlen($subtitle)==0) {
  $form_state->setErrorByName('subtitle', t('Subtitle must contain at least one character'));
}
if ($network['tweeter']=="0" && $network['facebook']=="0"){
  $form_state->setErrorByName('network', t('Chosse at least one network'));
}

La méthode build()

Doc : https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Element%21FormElement.php/class/hierarchy/FormElement/8.8.x

La méthode build() va gérer l’affichage du bloc. Elle est généralement placée en haut de la classe. Cependant,  j'ai préféré placer build() sous submit() pour que vous sachiez comment a été valorisé l'attribut configuration de la classe.

public function build() {
}

On commence par générer le sous titre à l'aide d'un tableau de rendu du type html_tag. Le code n'est exécuté que si dans la configuration la clé 'asksubtitle' ='yes' et si la clé 'subtitle' n'est pas vide. 

 //Génére le sous titre du bloc
    if ($this->configuration['asksubtitle']=='yes' && strlen($this->configuration['subtitle'])>0){
      $build['subtitle']=[
        '#type' => 'html_tag',
        '#tag' => 'h2',
        '#value' => $this->configuration['subtitle'],
      ];
    }

Ensuite on génère le lien twitter à l'aide d'un autre tableau de rendu seulement si l'utilisateur a coché la case twitter :

//Création d'un lien aec un render array
    global $base_url;
    
    if ($this->configuration['network']['twitter']=='twitter'){
    $url = Url::fromUri('https://twitter.com/share?url='.$base_url.Url::fromRoute('<current>')->toString());
    $pathmodule=drupal_get_path('module', 'share_configurable');
    $uritwitter=$pathmodule.'/src/img/twitter.png';

      $build['link_twitter'] = [
        '#title' => [
          '#type' => 'html_tag',
          '#tag' => 'img',
          '#attributes'=>[
            'src' => $uritwitter,
            'height' => '30',
            'width' => '30',
          ]
        ],
        '#type' => 'link',
        '#url' => $url
      ];
    }

Vous remarquerez que l'attribut title, qui correspond à l'ancre du lien, est constitué d'un autre render array pour générer l'image.

Les url sont obtenues à l'aide des méthode fromUri() et FromRoute() de la classe url comme expliqué dans l'article créer un lien depuis le contrôleur

Enfin on passe les différentes variables dans une variable unique appelé $variable et qui sera déclarée dans le fichier nommodule.module

 $variable['build']=$build;
    $variable['config']=$this->configuration;

Il ne reste plus qu'à retourner dans un tableau associatif le thème à utiliser, le titre et la description du bloc, et la variable :

return [
      '#theme' => 'blockboutonpartagechoix',
      '#title' => t('Share this page !'),
      '#description' => t('a bloc to share content with facebook or twitter'),
      '#variable' => $variable,

    ];

    Déclarer le thème et indiquer le gabarit à utiliser

    Pour pouvoir utiliser un gabarit twig, vous devez déclarer un thème dans la fonction nomtheme_theme du fichier nomtheme_module :

    <?php
    function premiermo_theme($existing, $type, $theme, $path) {
      return [ 
        'blockboutonpartagechoix' => [
      'variables' => [
        'config'=>'',
      ],
      'template' => 'blockboutonpartagechoix',
    ]
      ];
    }

    La clé 'blockboutonpartagechoix' du tableau renvoyé par cette fonction, doit absolument correspondre à la clé « thème » renvoyé par la fonction build() du fichier .php créé dans la section précédente. Cette clé a pour valeur un tableau associatif qui doit contenir

    • une clé template qui a pour valeur le nom du gabarit twig utilisé pour mettre en forme le bloc. Le nom du gabarit ne doit pas contenir les extensions .html.twig
    • une clé variables dont la valeur est un tableau associatif qui contient des variables et leur valeur qui sont utilisables dans le gabarit

    Le fichier templates/nomdugabarit.html.twig

    Créer un fichier templates/blockboutonpartagechoix.html.twig. Le nom du gabarit doit absolument être celui déclaré dans le fichier nomtheme.module.

    Pour utiliser des images, il est possible de créer un répertoire modules/custom/premiermo/src/img/ et d’y faire référence dans le gabarit twig.

    Pour récupérer l’url courante, on utilise la fonction twig

    {{ url('<current>') }}

    La fonction build() a passé la variable config qui contient les éléments de configuration du bloc stockés en base. Il ne reste plus qu’à faire des tests pour voir les éléments que l’on affiche :

    {% if config['asksubtitle']is same as('yes') and config['subtitle']|length>0 %}
      <h2>{{ config['subtitle']}}</h2>
    {% endif %}
    
    {% if config['network']['facebook']is same as('facebook') %}
    <a target="_blank" title="Facebook" href="https://www.facebook.com/sharer/sharer.php?u={{ url('<current>') }}" rel="nofollow"><img src="modules/custom/premiermo/src/img/facebook.png" alt="Facebook" /></a>
    {% endif %}
    
    {% if config['network']['tweeter']is same as('tweeter') %}
    <a target="_blank" title="Twitter" href="https://www.facebook.com/sharer/sharer.php?u={{ url('<current>') }}" rel="nofollow"><img src="modules/custom/premiermo/src/img/tweeter.png" alt="Tweetter" width="50px" height="50px"/></a>
    {% endif %}

     

    Documents utiles
    Version