Dynamically override a configuration with Drupal 8

des manettes d'ouverture sur des tuyaux

In some cases, it can be extremely interesting to be able to override a configuration dynamically. One of the first use cases immediately noticeable is in the case of a site factory with a set of shared and deployed features, and therefore identical configurations shared. And this on a website factory that is based on the native multi-site architecture of Drupal 8, or based on modules like Domain or Micro site that can host multiple sites from a single instance Drupal 8.

The immediate benefit is to be able to dynamically use and configure the contributed Drupal modules in the context of an instance deployed on a site factory, and thus to benefit from all the work already done, while modifying only certain configuration elements of the module to adapt it to the context where it is used.

Let's move on to practice, on a concrete example. Imagine in the context of a site factory, a galaxy of sites with a set of shared taxonomy vocabularies. From a maintenance point of view, the advantage is to be able to maintain and modify any configuration related to these vocabularies on all the sites that use it. From the point of view of each site, the major issue is that the vocabulary name (and therefore its possible uses in field labels, automatically generated aliases, etc.) can and must differ.

We have several methods to overload the vocabulary name for each site. The simplest method, in the case of a multi-site architecture is to override it from the settings.php file of each instance.

$config['taxonomy.vocabulary.VOCABULARY_ID']['name'] = 'NEW_VOCABULATY_LABEL';

And it's over. But this method requires access to the server, and is therefore not administrable from a site instance itself by an administrator or a webmaster. In addition, this method can only be used in the context of a website factory based on Drupal's multi-site architecture, and not in the context of a Domain or Micro site based website factory, since both architectures actually use one and the same Drupal 8 instance.

A second method is to provide a configuration form that will be responsible for changing the vocabulary labels for each site instance.

For example

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    foreach ($this->vocabularies as $id => $label) {
      $original_label = $this->configFactory->get('taxonomy.vocabulary.' . $id)->getOriginal('name', FALSE);
      $form[$id] = [
        '#type' => 'fieldset',
        '#title' => $label . ' (original : ' . $original_label . ')',
      ];
     
      $form[$id][$id . '_label'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Label'),
        '#maxlength' => 64,
        '#size' => 64,
        '#default_value' => ($this->settings->get('vocabularies.' . $id . '.label')) ?: $label,
      ];

      $options_widget = [
        '' => $this->t('Default'),
        'entity_reference_autocomplete' => $this->t('Autocompletion (with automatic creation)'),
        'options_buttons' => $this->t('Checkboxes (without automatic creation)'),
        'options_select' => $this->t('Select list (without automatic creation)'),
      ];
      $form[$id][$id . '_widget'] = [
        '#type' => 'select',
        '#title' => $this->t('Widget'),
        '#options' => $options_widget,
        '#default_value' => ($this->settings->get('vocabularies.' . $id . '.widget')) ?: '',
      ];
      
    }

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    parent::submitForm($form, $form_state);

    foreach ($this->vocabularies as $id => $label) {
      $this->settings->set('vocabularies.' . $id . '.label', $form_state->getValue($id . '_label'));
      $this->settings->set('vocabularies.' . $id . '.widget', $form_state->getValue($id . '_widget'));
    }

    // We need to store here the node types and vocabularies because we can not
    // call the entity type manager in the TaxonomyConfigOverrides service
    // because of a circular reference.
    $this->settings->set('node', $this->getNodeTypeIds(FALSE));
    $this->settings->set('taxonomy_vocabulary', $this->getVocabularyIds(FALSE));

    $this->settings->save();
    drupal_flush_all_caches();
  }

}

As you can see, we can override a lot more than the simple label of a vocabulary. For example the widget used in content editing forms. And many other things.

Then once this form in place and its configuration elements saved, we just need to create a service that will implement the service config.factory.override.

services:

  MY_MODULE.taxonomy_overrider:
    class: \Drupal\MY_MODULE\TaxonomyConfigOverrides
    tags:
      - {name: config.factory.override, priority: 300}
    arguments: ['@config.factory']

 

And we create the class TaxonomyConfigOverrides

<?php

namespace Drupal\MY_MODULE;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\StorageInterface;

/**
 * Override Taxonomy configuration per micro site.
 *
 * @package Drupal\MY_MODULE
 */
class TaxonomyConfigOverrides implements ConfigFactoryOverrideInterface {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;
  
  /**
   * An array of all the vocabulary ids.
   *
   * @var array
   */
  protected $vocabularies;

  /**
   * An array of all the node type ids.
   *
   * @var array
   */
  protected $nodeTypes;

  /**
   * The settings of the factory taxonomy configuration.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $taxonomySettings;

  /**
   * Constructs the object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
    $this->taxonomySettings = $this->configFactory->get('MY_MODULE.taxonomy_settings');
    $this->vocabularies = $this->getVocabularyIds();
  }

  /**
   * {@inheritdoc}
   */
  public function loadOverrides($names) {
    $overrides = [];

    foreach ($this->vocabularies as $vocabularyId) {
      $label = $this->taxonomySettings->get('vocabularies.' . $vocabularyId . '.label');
      if (empty($label)) {
        continue;
      }

      $vocabulary_config_id = $this->getVocabularyConfigName($vocabularyId);
      if (in_array($vocabulary_config_id, $names)) {
        $overrides[$vocabulary_config_id]['name'] = $label;
      }

    }

    return $overrides;
  }

  /**
   * @param $vocabularyId
   * @return string
   */
  protected function getVocabularyConfigName($vocabularyId) {
    return "taxonomy.vocabulary.{$vocabularyId}";
  }

  /**
   * Get an array of vocabulary id.
   *
   * @return array
   *   An array of vocabulary id.
   */
  protected function getVocabularyIds() {
    return $this->taxonomySettings->get('taxonomy_vocabulary') ?: [];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheSuffix() {
    return 'config_taxonomy_label';
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheableMetadata($name) {
    $meta = new CacheableMetadata();
    $meta->addCacheableDependency($this->taxonomySettings);
    return $meta;
  }

  /**
   * {@inheritdoc}
   */
  public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
    return NULL;
  }

}

And so you can now share a common shared configuration on a multitude of sites, while allowing everyone to customize at will different configuration elements according to their needs.

Override vocabulary settings

Of course, this method must be used according to the needs and architecture of the site factory. Using such a method in the context of a multi-site architecture for anything and everything is not necessarily the best way. Neither the most optimum. When it can be enough simply to provide initial configuration elements, for this or that module, by the code and then let him live his life within the instance of the website factory.

Do not hesitate to seek advice from a Drupal 8 developer who can define with you the best strategy to ensure that your site factory has all the assets to evolve with confidence, and to gain maturity in the long term. 

 

Ajouter un commentaire