Drupal 8 - Finalisation du formulaire de configuration globale

Finalisation du formulaire de configuration globale

Dans les précédents articles sur la Création d'un module avec Drupal 8, nous avons vu comment créer une entité de type configuration, ainsi qu'un élément de formulaire qui pourra y être associé.

Nous allons maintenant mettre en place le formulaire qui va servir à afficher et configurer notre module.

Introduction

Après avoir effectué des recherches et pas mal de tests, j'ai fini par m'inspirer du système de configuration du module "Content Translation". Nous utiliserons hook_theme pour intégrer deux templates de formulaires, nous ferons une injection de dépendances pour intégrer le service "entity_type.manager" à notre formulaire, puis nous implémenterons les méthodes buildForm, validateForm et submitForm.

Base du formulaire

Pour commencer, ajoutons les services nécessaires et les squelettes de fonctions pour la suite:

/**
 * @file
 * Contains \Drupal\internal_link\Form\InternalLinkSettingsForm.
 */
 
namespace Drupal\internal_link\Form;
 
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\EntityDefinitionUpdateManager;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\internal_link\Entity\InternalLinkSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
/**
 * Class InternalLinkSettingsForm.
 *
 * @package Drupal\internal_link\Form
 *
 * @ingroup internal_link
 */
class InternalLinkSettingsForm extends FormBase {
 
  /**
   * Entity type manager definition.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;
 
  /**
   * Entity definition update manager definition.
   *
   * @var \Drupal\Core\Entity\EntityDefinitionUpdateManager
   */
  protected $entityDefinitionUpdateManager;
 
  /**
   * Constructor
   *
   * @param EntityTypeManagerInterface $entity_type_manager
   * @param EntityDefinitionUpdateManagerInterface $entity_definition_update_manager
   */
  public function __construct(EntityTypeManager $entity_type_manager, EntityDefinitionUpdateManager $entity_definition_update_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityDefinitionUpdateManager = $entity_definition_update_manager;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('entity.definition_update_manager')
    );
  }
 
  /**
   * Returns a unique string identifying the form.
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'InternalLink_settings';
  }
 
  /**
   * Defines the settings form for Internal link entities.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   Form definition array.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['InternalLink_settings']['#markup'] = 'Settings form for Internal link entities. Manage field settings here.';
    return $form;
  }
 
  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
  }
 
  /**
   * Form submission handler.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Empty implementation of the abstract submit class.
  }
}

Nous avons injecté les services "entity_type.manager" et "entity.definition_update_manager". Le premier doit normalement vous être familier, nous l'avons déjà utilisé. Le second permet de gérer les mises à jour d'entités. Il nous permettra, lors de la soumission du formulaire, de mettre à jour les entités et bundles afin de prendre en compte des modifications effectuées dessus.

Theming

Pour les besoins de notre formulaire de configuration, nous allons ensuite intégrer un peu de theming. Nous ajoutons d'abord 2 fonctions de preprocess dans hook_theme, qui va au final ressembler à ça:

/**
 * Implements hook_theme().
 */
function internal_link_theme() {
  return [
    'internal_link' => [
      'render element' => 'elements',
      'file' => 'internal_link.page.inc',
      'template' => 'internal_link',
    ],
    'internal_link_settings_entity_type_table' => [
      'render element' => 'element',
      'file' => 'internal_link.admin.inc',
    ],
    'internal_link_settings_bundle_table' => [
      'render element' => 'element',
      'file' => 'internal_link.admin.inc',
    ],
  ];
}

Nous allons également ajouter un nouveau fichier à la racine de notre module, nommé internal_link.admin.inc. C'est dans ce fichier que nous allons placer nos deux fonctions de preprocess:

use Drupal\Core\Render\Element;
 
/**
 * Prepares variables for internal link settings entity-type table templates.
 *
 * Default template: internal-link-settings-entity-type-table.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title, #description.
 */
function template_preprocess_internal_link_settings_entity_type_table(&$variables) {
  // Add a render element representing the entity-type settings table.
  $element = $variables['element'];
  $header = [
    [
      'data' => t('Entity type state'),
      'class' => ['entity_type'],
    ],
    [
      'data' => t('Configuration'),
      'class' => ['operations'],
    ],
  ];
 
  $rows = [];
  foreach (Element::children($element) as $entity_type) {
    $rows[$entity_type] = [
      'data' => [
        [
          'data' => $element[$entity_type]['enabled'],
          'class' => ['enabled'],
        ],
        [
          'data' => $element[$entity_type]['cardinality'],
          'class' => ['operations'],
        ],
      ],
      'class' => ['entity-type-settings'],
    ];
  }
 
  $variables['title'] = $element['#title'];
  $variables['description'] = $element['#description'];
  $variables['build'] = [
    '#header' => $header,
    '#rows' => $rows,
    '#type' => 'table',
  ];
}
 
 
 
/**
 * Prepares variables for internal link settings bundle table templates.
 *
 * Default template: internal-link-settings-bundle-table.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #bundle_label, #title.
 */
function template_preprocess_internal_link_settings_bundle_table(&$variables) {
  // Add a render element representing the bundle settings table.
  $element = $variables['element'];
 
  $header = [
    [
      'data' => $element['#bundle_label'],
      'class' => ['bundle'],
    ],
    [
      'data' => t('Configuration'),
      'class' => ['operations'],
    ],
  ];
 
  $rows = [];
  foreach (Element::children($element) as $bundle) {
    $rows[$bundle] = [
      'data' => [
        [
          'data' => [
            '#prefix' => '<label>',
            '#suffix' => '</label>',
            '#plain_text' => $element[$bundle]['settings']['#label'],
          ],
          'class' => ['bundle'],
        ],
        [
          'data' => $element[$bundle]['settings'],
          'class' => ['operations'],
        ],
      ],
      'class' => ['bundle-settings'],
    ];
  }
 
  $variables['title'] = $element['#title'];
  $variables['build'] = [
    '#header' => $header,
    '#rows' => $rows,
    '#type' => 'table',
  ];
}

Rien de vraiment complexe dans ces deux méthodes. Elles vont simplement servir à afficher les données sous forme de tableaux. Nous aurons un premier tableau permettant de sélectionner les types d'entité à associer avec les liens internes ainsi que la "cardinalité" à affecter au champ "internal_link" qui sera attaché à chaque bundle.

Un second tableau affichera pour chaque bundle la configuration possible.

Implémentation des méthodes importantes

Maintenant que nous avons la base, nous allons pouvoir implémenter chaque méthode importante afin d'arriver à un résultat concrèt.

Méthode buildForm

Cette méthode est celle qui affiche le formulaire de configuration. Son implémentation est la suivante:

  public function buildForm(array $form, FormStateInterface $form_state) {
    // Retrieve data settings to build form.
 
    list($entity_types, $labels, $default, $bundles, $fields, $configuration) = InternalLinkSettings::getFormSpecifications();
 
    // Start form.
    $form = [
      '#labels' => $labels,
      '#attributes' => [
        'class' => 'internal-link-settings-form',
      ],
    ];
 
    // Add entity types container.
    $form['entity_types'] = [
      '#title' => $this->t('Internal link settings'),
      '#description' => $this->t('You can choose here the entity types and bundles on which you want to associate internal links. Enable internal links for a particular content type will create an internal link field to associate words with entities of the chosen type.
Once you enabled an entity type, you can choose configuration for each bundle linked to the entity type.'),
      '#type' => 'container',
      '#theme' => 'internal_link_settings_entity_type_table',
      '#tree' => TRUE,
    ];
 
    // Prepare cardinality options.
    $options = [
      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED => $this->t('Unlimited'),
    ];
    for ($i = 1; $i <= 20; $i++) {
      $options[$i] = $i;
    }
 
    // Iterate over entity types.
    foreach ($labels as $entity_type_id => $label) {
      $entity_type = $entity_types[$entity_type_id];
 
      // Prepare sub-container.
      $form['entity_types'][$entity_type_id] = [
        '#type' => 'container',
        '#entity_type' => $entity_type_id,
        '#entity_type_label' => $label,
        '#tree' => TRUE,
      ];
 
      // Add "enabled" checkbox.
      $form['entity_types'][$entity_type_id]['enabled'] = [
        '#title' => $label,
        '#type' => 'checkbox',
        '#default_value' => $default[$entity_type_id]['enabled'],
      ];
 
      // Add "cardinality" settings.
      $cardinality = -1;
      if ($field_storage = FieldStorageConfig::loadByName($entity_type_id, 'internal_link')) {
        $cardinality = $field_storage->getCardinality();
      }
 
      $form['entity_types'][$entity_type_id]['cardinality'] = [
        '#title' => $this->t('Field cardinality'),
        '#description' => $this->t('Choose the field cardinality. Cardinality indicate how many items you can select in the field.'),
        '#type' => 'select',
        '#options' => $options,
        '#default_value' => $cardinality,
        '#states' => [
          'visible' => [
            ':input[name="entity_types[' . $entity_type_id . '][enabled]"]' => ['checked' => TRUE],
          ],
        ],
      ];
    }
 
    // Add entity-types/bundles settings container.
    $form['settings'] = [
      '#tree' => TRUE,
    ];
 
    // Iterate over each entity types.
    foreach ($labels as $entity_type_id => $label) {
      $entity_type = $entity_types[$entity_type_id];
 
      // Build settings container for each entity type.
      $form['settings'][$entity_type_id] = [
        '#title' => $label,
        '#type' => 'container',
        '#entity_type' => $entity_type_id,
        '#theme' => 'internal_link_settings_bundle_table',
        '#bundle_label' => $entity_type->getBundleLabel() ?: $label,
        '#states' => [
          'visible' => [
            ':input[name="entity_types[' . $entity_type_id . '][enabled]"]' => ['checked' => TRUE],
          ],
        ],
      ];
 
      // Iterate over each bundle.
      foreach ($bundles[$entity_type_id] as $bundle => $bundle_info) {
        if (isset($fields[$entity_type_id][$bundle])) {
 
          // Add settings item with internal-link configuration field type.
          $form['settings'][$entity_type_id][$bundle]['settings'] = [
            '#type' => 'item',
            '#label' => $bundle_info['label'],
            'internal_link' => [
              '#type' => 'internal_link_configuration',
              '#options' => [
                'fields' => $fields[$entity_type_id][$bundle],
              ],
              '#default_value' => $configuration[$entity_type_id][$bundle],
            ],
          ];
        }
      }
    }
 
    // Add submit button.
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save configuration'),
      '#button_type' => 'primary',
    ];
 
    return $form;
  }

La première étape est de récupérer, via la méthode statique InternalLinkSettings::getFormSpecifications les informations nécessaires à la construction de notre formulaire.

Une fois les informations en notre possession, nous commençons par ajouter un conteneur pour les types d'entité. Ce conteneur utiliser la fonction de preprocess correspondante afin d'afficher une case à cocher (activé/désactivé) et une liste déroulante (cardinalité du champ).

La cardinalité d'un champ se situe au niveau de la configuration "FieldStorage". Pour la petite explication, un champ X est créé pour un type d'entité, puis il peut être utilisé par n bundles. Le champ est donc global à tous les les bundles d'un type d'entité. Sa cardinalité également. Elle fait partie de la configuration au niveau du champ, puis, selon le type de champ, nous pouvons avoir de la configuration au niveau de l'instance de champ (en admettant qu'une "instance de champ" correspond à un champ attaché à un bundle particulier).

Nous rajoutons ensuite la partie "settings" dans notre formulaire, qui va permettre d'afficher la configuration "par bundle". Une itération est effectuée (boucle foreach) pour créer une entrée par type d'entité, puis une seconde itération est effectuée pour créer une entrée par bundle. C'est ici que nous commençons à assembler les éléments que nous avons implémentés dans les précédents articles:

          // Add settings item with internal-link configuration field type.
          $form['settings'][$entity_type_id][$bundle]['settings'] = [
            '#type' => 'item',
            '#label' => $bundle_info['label'],
            'internal_link' => [
              '#type' => 'internal_link_configuration',
              '#options' => [
                'fields' => $fields[$entity_type_id][$bundle],
              ],
              '#default_value' => $configuration[$entity_type_id][$bundle],
            ],
          ];

Cet élément de formulaire est de #type "item", mais possède une clé "internal_link" elle-même de #type "internal_link_configuration. Souvenez-vous, c'est le #type du @FormElement que nous avons créé précédemment. Nous lui passons en #options un tableau contenant les "champs" qui doivent être parsés pour la détection de liens internes.

Enfin, nous ajoutons une partie "actions" qui va correspondre au bouton de soumission du formulaire.

Si nous récapitulons:

  1. $form['entity_types'] va lister les types d'entité, chacun accompagné d'une case a cocher permettant d'activer ou de désactiver les liens internes sur le type d'entité associé, ainsi qu'une liste déroulante permettant de sélectionner la "cardinalité" du champ (FieldType/FieldWidget) qui sera rattaché à chaque bundle actif.
  2. $form['settings'] va lister quant à lui tous les bundles disponibles pour chaque type d'entité coché, en utilisant notre @FormElement pour l'affichage et le paramétrage de la configuration propre à chaque bundle.

ça semble un peu abstrait comme ça, mais videz votre cache à l'aide de Drupal Console:

drupal cr all

et naviguez à l'url /admin/config/search/internal_link/settings et vous verrez le formulaire implémenté. Si vous cochez un type d'entité s'affichera alors la liste déroulante de cardinalité, ainsi que la liste des bundles dans le tableau suivant, accompagné par notre @FormElement spécifique pour l'affichage et la sélection de la configuration des liens internes.

Méthode validateForm

Cette méthode est déclenchée lors de la soumission du formulaire. Elle est exécutée avant la méthode submitForm, afin de contrôler que toutes les informations sont correctes avant l'enregistrement des données dans le système. Dans notre cas, le contrôle à effectuer se limite à être certain que pour chaque bundle activé, au minimum un champ de parsing a été sélectionné par l'utilisateur. Si aucun champ n'est sélectionné, alors il n'y a pas d'intérêt d'activer les liens internes pour le bundle, puisque le parsing ne s'effectuera pas.

  public function validateForm(array &$form, FormStateInterface $form_state) {
 
    // Retrieve user configuration.
    $entity_types = $form_state->getValue('entity_types', []);
    $settings = $form_state->getValue('settings', []);
 
    // Iterate over each settings.
    foreach ($settings as $entity_type => $entity_settings) {
 
      // Check if current entity type is enabled.
      if ($entity_types[$entity_type]['enabled']) {
        foreach ($entity_settings as $bundle => $bundle_settings) {
 
          // Get configuration settings for the current entity-type / bundle.
          $config_settings = $bundle_settings['settings']['internal_link'];
 
          // Check if internal-link is enabled.
          if ($config_settings['automatic_enabled'] || $config_settings['manual_enabled']) {
 
            // Retrieve enabled fields and set an error
            // if no one is selected.
            $fields = array_filter($config_settings['fields']);
            if (!count($fields)) {
              $form_state->setError(
                $form['settings'][$entity_type][$bundle]['settings']['internal_link']['fields'], 
                $this->t('You must choose at least one field to enable internal links on @bundle of type @entity_type.', [
                  '@bundle' => $bundle,
                  '@entity_type' => $entity_type,
                ])
              );
            }
          }
        }
      }
    }
  }

Le processus itère sur chaque type d'entité activé, puis sur chaque bundle appartenant au type d'entité, afin de tester si les liens internes sont actifs. Si c'est le cas, le processus va simplement tester que le tableau "fields" n'est pas vide.

Pour une meilleure validation, nous pourrions également rajouter d'autres tests, tels que le format utilisé pour les balises HTML dans le champ "Disallowed HTML Tags" par exemple.

SubmitForm

C'est la méthode la plus importante car elle va s'occuper de sauvegarder la configuration de chaque bundle. Vous l'avez peut-être remarqué: il n'y a pas de données sauvegardée pour le type d'entité en lui-même. Ce sont les configurations par bundle qui vont déterminer si oui ou non un type d'entité doit être coché dans le formulaire de configuration. De même, la valeur de la liste déroulante cardinalité est directement récupérée via le champ correspondant si il existe.

Voyons donc l'implémentation de la sauvegarde:

  public function submitForm(array &$form, FormStateInterface $form_state) {
 
    // Get user configuration.
    $entity_types = $form_state->getValue('entity_types', []);
    $settings = $form_state->getValue('settings', []);
 
    // Get default configuration settings.
    $default_config_settings = InternalLinkSettings::getDefaultConfigurationAsArray();   
 
    // Prepare a flag array to know if we
    // must remove the field-storage entity.
    $storage_exists = [];
 
    // Iterate over each entity-type.
    foreach ($settings as $entity_type => $entity_settings) {
 
      // Set flag array to default values.
      $storage_exists[$entity_type] = [
        'exists' => FALSE,
        'remove' => TRUE,
      ];
 
      // Retrieve field-storage configuration if possible.
      $field_storage = FieldStorageConfig::loadByName($entity_type, 'internal_link');
      if ($field_storage) {
        $storage_exists[$entity_type]['exists'] = TRUE;
 
        // Change cardinality if needed.
        if ($field_storage->getCardinality() != $entity_types[$entity_type]['cardinality']) {
          $field_storage->setCardinality($entity_types[$entity_type]['cardinality'])->save();
        }
      }
 
      // Iterate over each bundle.
      foreach ($entity_settings as $bundle => $bundle_settings) {
 
        // By default, use default configuration settings.
        $config_settings = $default_config_settings;
 
        // Check if current entity type is enabled
        // and retrieve current configuration settings.
        if ($entity_types[$entity_type]['enabled']) {
          $config_settings = $bundle_settings['settings']['internal_link'];
        }
 
        // Load current configuration from system.
        $config = InternalLinkSettings::loadByEntityTypeBundle($entity_type, $bundle);
 
        // Set configuration settings and save it.
        $config->setAutomaticEnabled($config_settings['automatic_enabled'])
          ->setManualEnabled($config_settings['manual_enabled'])
          ->setFields(array_filter($config_settings['fields']))
          ->setWrapHTMLTag($config_settings['wrap_html_tag'])
          ->setDisallowedHTMLTags($config_settings['disallowed_html_tags'])
          ->setHighlightWords($config_settings['highlight_words'])
          ->setWordBoundary($config_settings['word_boundary'])
          ->setLinksToProcess($config_settings['links_to_process'])
          ->setMaximumApplication($config_settings['maximum_application'])
          ->save();
 
        // Retrieve field configuration if possible.
        $field = FieldConfig::loadByName($entity_type, $bundle, 'internal_link');
 
        // Check if manual internal links is enabled.
        if ($config_settings['manual_enabled']) {
 
          // No field storage configuration ? create it.
          if (!$field_storage) {
 
            // @see \Drupal\field\Entity\FieldStorageConfig
 
            // Save the new field storage entity and reload it.
            $this->entityTypeManager->getStorage('field_storage_config')->create([
              'field_name' => 'internal_link',
              'entity_type' => $entity_type,
              'type' => 'internal_link_field',
              'translatable' => FALSE,
              //'locked' => TRUE, // to disallow modifications by user.
              'cardinality' => $entity_types[$entity_type]['cardinality'],
            ])->save();
            $field_storage = FieldStorageConfig::loadByName($entity_type, 'internal_link');
            $storage_exists[$entity_type]['exists'] = TRUE;
          }
 
          // No field configuration ? create it.
          if (!$field) {
            $this->entityTypeManager->getStorage('field_config')->create([
              'field_storage' => $field_storage,
              'entity_type' => $entity_type,
              'bundle' => $bundle,
              'label' => t('Internal links'),
              'field_name' => 'internal_link',
              'field_type' => 'internal_link_field',
            ])->save();
 
            // Entity form displays: assign widget settings for the 'default' form
            // mode, and hide the field in all other form modes.
            $this->entityTypeManager->getStorage('entity_form_display')
              ->load($entity_type.'.'.$bundle.'.'.'default')
              ->setComponent("internal_link", [
                'type' => 'internal_link_widget',
                'weight' => 20,
              ])
              ->save();
          }
          $storage_exists[$entity_type]['remove'] = FALSE;
        }
        else {
          if (isset($field)) {
            $field->delete();
          }
        }
      }
      // Do not remove it, it made it itself when 
      // the key "persist_with_no_fields" is set to false (default)
      //
      //if ($storage_exists[$entity_type]['exists']
      //    && $storage_exists[$entity_type]['remove']) {
      //  
      //  $field_storage->delete();
      //}
    }
    // Clear cache and apply updates to field-storage/field.
    $this->entityTypeManager->clearCachedDefinitions();
    $this->entityDefinitionUpdateManager->applyUpdates();
 
    drupal_set_message($this->t('Settings successfully updated.'));
  }

La première étape est de récupérer les données du formulaire via le $form_state:

    // Get user configuration.
    $entity_types = $form_state->getValue('entity_types', []);
    $settings = $form_state->getValue('settings', []);

Nous récupérons ensuite les valeurs par défaut de configuration, et nous créons un tableau de stockage qui déterminera au fur et à mesure du processus si le champ à attacher à un bundle particulier existe déjà ou si il est à créer:

    // Get default configuration settings.
    $default_config_settings = InternalLinkSettings::getDefaultConfigurationAsArray();   
 
    // Prepare a flag array to know if we
    // must remove the field-storage entity.
    $storage_exists = [];

Nous itérons ensuite sur chaque type d'entité et initialisons notre tableau de stockage en indiquant par défaut que le champ n'existe pas (exists => FALSE) et qu'il est à supprimer (remove => TRUE).

Nous tentons ensuite de récupérer la configuration de stockage du champ. Il faut bien faire la distinction entre la configuration de stockage du champ (ou l'information de stockage du champ, FieldStorageConfig) qui est associée au type d'entité, et la configuration du champ (ou l'information d'instance de champ, FieldConfig), qui est associée au bundle. Nous parlons ici bien des informations de stockage du champ, dont les données sont liées au type d'entité:

      // Retrieve field-storage configuration if possible.
      $field_storage = FieldStorageConfig::loadByName($entity_type, 'internal_link');
      if ($field_storage) {
        $storage_exists[$entity_type]['exists'] = TRUE;
 
        // Change cardinality if needed.
        if ($field_storage->getCardinality() != $entity_types[$entity_type]['cardinality']) {
          $field_storage->setCardinality($entity_types[$entity_type]['cardinality'])->save();
        }
      }

Si la configuration de stockage du champ existe alors nous changeons la valeur 'exists' et nous testons si la valeur de cardinalité du champ a été changée par l'utilisateur. Si c'est le cas, nous affectons la nouvelle cardinalité et sauvegardons cette configuration modifiée.

Nous itérons ensuite sur chaque bundle. La première chose à faire est d'affecter la configuration internal_link_settings par défaut, puis de tenter de récupérer la configuration actuelle issue du formulaire:

        // By default, use default configuration settings.
        $config_settings = $default_config_settings;
 
        // Check if current entity type is enabled
        // and retrieve current configuration settings.
        if ($entity_types[$entity_type]['enabled']) {
          $config_settings = $bundle_settings['settings']['internal_link'];
        }

Nous récupérons ensuite la configuration actuelle enregistrée dans le système (si elle existe) et nous l'écrasons par celle issue du formulaire:

        // Load current configuration from system.
        $config = InternalLinkSettings::loadByEntityTypeBundle($entity_type, $bundle);
 
        // Set configuration settings and save it.
        $config->setAutomaticEnabled($config_settings['automatic_enabled'])
          ->setManualEnabled($config_settings['manual_enabled'])
          ->setFields(array_filter($config_settings['fields']))
          ->setWrapHTMLTag($config_settings['wrap_html_tag'])
          ->setDisallowedHTMLTags($config_settings['disallowed_html_tags'])
          ->setHighlightWords($config_settings['highlight_words'])
          ->setWordBoundary($config_settings['word_boundary'])
          ->setLinksToProcess($config_settings['links_to_process'])
          ->setMaximumApplication($config_settings['maximum_application'])
          ->save();

Nous passons maintenant à la partie @FieldType/@FieldWidget. Souvenez-vous de nos besoins: si le bundle autorise une sélection manuelle des liens internes, nous devons attacher le champ que nous avons créé au bundle, afin de proposer au rédacteur un champ de sélection des liens internes pour chaque contenu qu'il va créer.

Nous tentons donc de récupérer la configuration actuelle de l'instance de champ pour le bundle en cours de traitement et nous testons si la configuration manuelle est activée:

        // Retrieve field configuration if possible.
        $field = FieldConfig::loadByName($entity_type, $bundle, 'internal_link');
 
        // Check if manual internal links is enabled.
        if ($config_settings['manual_enabled']) {

Nous allons ensuite tester si la configuration de stockage du champ (au niveau du type d'entité) existe déjà. Si ce n'est pas le cas, nous allons le créer avec le code suivant:

          // No field storage configuration ? create it.
          if (!$field_storage) {
 
            // @see \Drupal\field\Entity\FieldStorageConfig
 
            // Save the new field storage entity and reload it.
            $this->entityTypeManager->getStorage('field_storage_config')->create([
              'field_name' => 'internal_link',
              'entity_type' => $entity_type,
              'type' => 'internal_link_field',
              'translatable' => FALSE,
              //'locked' => TRUE, // to disallow modifications by user.
              'cardinality' => $entity_types[$entity_type]['cardinality'],
            ])->save();
            $field_storage = FieldStorageConfig::loadByName($entity_type, 'internal_link');
            $storage_exists[$entity_type]['exists'] = TRUE;
          }

Ce code permet de récupérer le gestionnaire de stockage des entités de type "Field" via getStorage('field_storage_config') et de créer une nouvelle configuration de stockage via create et save. Notre champ est de @FieldType "internal_link_field" et utilisera donc par défaut le @FieldWidget "internal_link_widget" puisque c'est la configuration que nous avions indiquée dans l'annotation du @FieldType "internal_link_field".

Maintenant que nous sommes sur que la configuration de stockage du champ existe dans le système, nous devons encore nous assurer qu'une instance de champ utilisant cette configuration est bien rattachée à notre bundle. Cette étape s'effectue avec le code suivant:

          // No field configuration ? create it.
          if (!$field) {
            $this->entityTypeManager->getStorage('field_config')->create([
              'field_storage' => $field_storage,
              'entity_type' => $entity_type,
              'bundle' => $bundle,
              'label' => t('Internal links'),
              'field_name' => 'internal_link',
              'field_type' => 'internal_link_field',
            ])->save();
 
            // Entity form displays: assign widget settings for the 'default' form
            // mode, and hide the field in all other form modes.
            $this->entityTypeManager->getStorage('entity_form_display')
              ->load($entity_type.'.'.$bundle.'.'.'default')
              ->setComponent("internal_link", [
                'type' => 'internal_link_widget',
                'weight' => 20,
              ])
              ->save();

Nous testons si l'instance de champ existe, et si ce n'est pas le cas, nous la créons. Vous constaterez que précédemment, nous avons utilisé getStorage('field_storage_config') et cette fois-ci, nous utilisons getStorage('field_config'). Comme indiqué, la première étape consistait à traiter le champ au niveau du type d'entité (FieldStorageConfig), alors que cette fois, nous traitons le champ au niveau du bundle (FieldConfig), donc "l'instance" de champ rattachée à un bundle spécifique.

La première partie va rattacher le champ au bundle, tandis que la seconde va indiquer le comportement de notre champ, ou plutôt de son widget en terme d'affichage dans le formulaire, via la méthode setComponent.

Nous indiquons enfin dans notre tableau de stockage que le champ n'est pas à supprimer:

$storage_exists[$entity_type]['remove'] = FALSE;

Dans le cas ou la configuration manuelle n'est pas activée mais que l'instance de champ existe pour le bundle en cours de traitement, nous utilisons la méthode delete de l'instance de champ pour le supprimer:

        else {
          if (isset($field)) {
            $field->delete();
          }
        }

Nous trouvons un code commenté vers la fin de l'implémentation. Je pensais au départ que les informations de stockage d'un champ devait être supprimée manuellement si plus aucune instance de champ ne l'utilisait. En réalité, la classe FieldStorageConfig possède une variable $persist_with_no_fields qui est par défaut initialisée à FALSE. Cette variable, comme son nom l'indique, défini si un champ doit être supprimé de manière automatique dès lors que plus aucune instance de champ ne l'utilise. Nul besoin de s'en faire quant à la persistance de la configuration de stockage puisqu'elle est supprimée automatiquement.

Reste enfin une partie très importante du processus, celle de s'assurer que les modifications opérées sont prises en compte dès lors que le formulaire a été soumi:

    // Clear cache and apply updates to field-storage/field.
    $this->entityTypeManager->clearCachedDefinitions();
    $this->entityDefinitionUpdateManager->applyUpdates();
 
    drupal_set_message($this->t('Settings successfully updated.'));

Le service entity_type.manager exécute la méthode clearCachedDefinitions pour mettre à jour les définitions de plugins dans le cache.

Le service entity.definition_update_manager exécute la méthode applyUpdates pour appliquer tous les changements valides sur les entités.

Ces deux appels font en quelque sorte un "vidage de cache" pour tout ce qui concerne la configuration de stockage de champs et les instances de champ, afin de mettre à jour le système et s'assurer que les champs ajoutés ou supprimés ont été validés.

La partie code est finalisée. Si vous naviguez sur le panneau de configuration, vous verrez les choses suivantes:

Tout d'abord, la partie haute permet de sélectionner les types d'entités qui seront soumis au parsing de liens internes, tel que le montre la copie d'écran ci-dessous:

Configuration du module, activation des liens internes pour un type d'entité

Une fois un ou plusieurs types d'entité sélectionnés, la partie basse liste les différents bundles associés à chaque type d'entité, et pour chaque bundle la configuration possible/sélectionnée:

Configuration du module, affichage de la configuration par bundle

Conclusion

Cette partie a demandé un gros travail, non seulement de compréhension mais également de combinaison afin d'arriver au but final. Nous avons travaillé sur la mise en place d'un formulaire de configuration en utilisant :

  • un type d'entité configuration pour le stockage de la configuration dans le système;
  • un @FormElement permettant de gérer proprement les données de configuration de notre entité internal_link;
  • un nouveau type de champ et son widget associé, en héritant de deux classes existantes et en codant un minimum de choses;
  • la création "à la volée" d'une configuration de stockage de champ et le rattachement d'une instance de champ à un bundle.

Tous ces éléments mis ensemble nous ont permis de finaliser un formulaire de configuration qui laisse à l'utilisateur le choix d'activer et de configurer le système de liens internes "par bundle".

La plus grosse part de notre module est maintenant achevée.

Commentaires

Ajouter un commentaire

CAPTCHA
Cette question permet de savoir si vous êtes un visiteur ou un robot de soumission de spams