This project is not covered by Drupal’s security advisory policy.

On many project I had to make complicated settings form for custom modules, and I must said it is always a painful experience to declare the settings form extending ConfigFormBase, specially the buildForm and submitForm methods.

For a long time already, I use a custom module of mine where I extend the ConfigFormBase with a more helpful one, where I just have to pass the fields list to save, and some properties. But I still had to build the form.

ConfigFormBase::buildForm

The buildForm method has to define the form render array, with all the properties, which are mainly static ones, and to translate titles and descriptions.

Often, the use of short name for input is not really smart, because you want to have the browser proposing some values specific to the context, and not general values (like email) you can use on generic form you fill all over the web.

For some purposes, you need to have dynamically loaded values (options) from custom functions, or to display specific data from the context. But most of the time you just declare a list of arrays with properties from the render element list we all play with.

ConfigFormBase::submitForm

The submitForm method is even more repetitive, because it's just there to tell Drupal which input value set inside which settings field.

Since I have a tools module with a more elaborate ConfigFormBase, I do not write it at all.

This method is not helped by the trait, except in the fact you can use the fields array property to loop inside the set of fields to save.

FormGenerateTrait

The idea of this trait came to me when I was developing a new module, once again with a complicated settings form full of fields to follow the parameters sets of a JS library.

All those parameters where specified on the library documentation, in a HTML table, including title, type (or kind of type), default value and a long description.

When you also have to split them in details with vertical_tabs display, also with a title, and end up with more than 100 items in your form array, you really start to try to solve this otherwise.

Few time earlier, I just start playing with the powerfull YML tools provided by Drupal, and I was really thinking something has to be done with it.

Here is the code, fill free to use it or to improve it.

<?php

namespace Drupal\atools\Form;

use Drupal\Core\Config\FileStorage;

/**
 * Provide method to generate FORM from YML configuration files.
 *
 * This trait provides a method to load form configuration YML files.
 * With those declarations, it can generate a FORM
 * and list the fields to be saved.
 */
trait FormGenerateTrait {

  /**
   * The config form loaded from file.
   *
   * @var array|bool
   */
  private $configForm = FALSE;

  /**
   * List of config fields loaded from file.
   *
   * @var array|bool
   */
  private $fields = FALSE;

  /**
   * List of default fields values from file.
   *
   * @var array|bool
   */
  private $defaults = FALSE;

  /**
   * Load the YML configurations files for an extension.
   *
   * It is depending on the name provided by :
   * - getConfigFormName : for the form structure.
   * - getConfigFieldsName : for the list of fields.
   *
   * @param string $name
   *   The name of the extension to load from.
   * @param string $type
   *   (optional) The type of extension to load from.
   */
  protected function loadConf($name, $type = 'module') {
    $config_path = drupal_get_path($type, $name) . '/config/tools';
    $config_source = new FileStorage($config_path);

    $this->configForm = $config_source->read($this->getConfigFormName());
    $this->fields = $config_source->read($this->getConfigFieldsName());

    // If the fields array is associative, default values are provided.
    if ($this->fields !== FALSE && !isset($this->fields[0])) {
      $this->defaults = $this->fields;
      $this->fields = array_keys($this->fields);
    }
  }

  /**
   * Get the generated form based on fields from file.
   *
   * @return array
   *   Result form.
   */
  protected function getGeneratedForm() {
    return ($this->configForm !== FALSE) ? $this->generateForm($this->configForm) : [];
  }

  /**
   * Generate additionnal fields on the settings form.
   *
   * It use the %NAME%.form.yml file configuration available in modules.
   *
   * @param array $conf
   *   An associative array containing the settings for the form.
   *
   * @return array
   *   The generated form.
   */
  protected function generateForm(array $conf) {
    static $renderElements = [
      'actions',
      'ajax',
      'container',
      'contextual_links',
      'contextual_links_placeholder',
      'details',
      'dropbutton',
      'field_ui_table',
      'fieldgroup',
      'fieldset',
      'form',
      'html',
      'html_tag',
      'inline_template',
      'label',
      'link',
      'inline_template',
      'label',
      'link',
      'more_link',
      'operations',
      'page',
      'page_title',
      'pager',
      'processed_text',
      'responsive_image',
      'status_messages',
      'system_compact_link',
      'text_format',
      'toolbar',
      'toolbar_item',
      'vertical_tabs',
      'view',
    ];
    $form = [];

    foreach ($conf as $key => $values) {
      $subForm = [];
      if (isset($values['_fields'])) {
        $subForm = $this->generateForm($values['_fields']);
        unset($values['_fields']);
      }
      $element = $this->generateField($values);

      if (!isset($element['#type']) || in_array($element['#type'], $renderElements)) {
        // Add the element to the form.
        $form[$key] = $element + $subForm;
      } else {
        // Add the default value.
        if ($default = $this->getDefaultFieldValue($key)) {
          $element['#default_value'] = $default;
        }
        // Add the element to the form.
        $form[$this->getFieldName($key)] = $element + $subForm;
      }
    }

    return $form;
  }

  /**
   * Generate a field for the form.
   *
   * @param array $conf
   *   The configuration for the element to generate.
   *
   * @return array
   *   The generated element.
   */
  protected function generateField(array $conf) {
    $element = [];

    foreach ($conf as $key => $value) {
      switch (TRUE) {
        case $key == 'title':
        case $key == 'description':
          $val = $this->t($value);
          break;

        case $key == 'markup':
          $val = '<div>' . $this->t($value) . '</div>';
          break;

        default:
          $val = $value;
          break;
      }
      $element['#' . $key] = $val;
    }

    return $element;
  }

  /**
   * Gets the settings form config name, to load config from YML file.
   *
   * @return string
   *   The name of the fields list conf.
   */
  protected function getConfigFormName() {
    return $this->getSchemaName() . '.form';
  }

  /**
   * Gets the fields list config name, to load fields list from YML file.
   *
   * @return string
   *   The name of the fields list conf.
   */
  protected function getConfigFieldsName() {
    return $this->getSchemaName() . '.fields';
  }

  /**
   * Gets the settings form config name, to load config from YML file.
   *
   * @return string
   *   The name of the fields list conf.
   */
  abstract protected function getSchemaName();

  /**
   * Return a unique name for a form field depending on the key.
   *
   * @param string $key
   *   The field key.
   *
   * @return string
   *   Form element name.
   */
  protected function getFieldName($key) {
    return $this->getSchemaName() . '-' . $key;
  }

  /**
   * Get a field default value from a key.
   *
   * @param string $key
   *   The field key.
   *
   * @return mixed
   *   The value.
   */
  protected function getDefaultFieldValue($key) {
    return isset($this->defaults[$key]) ? $this->defaults[$key] : NULL;
  }
}

YML declarations

You have to declare two files in your extension (mainly module) config/tools directory.

The names of the YML files are specified by the getSchemaName() abstract method. For instance you can use the settings form ID or the config name. 

XXX.fields.yml

The first one is simply the list of fields to save / set. It is a succession of field[: value] set in a YML way.

If you just need the fields list, because you override the getDefaultFieldValue() method in your form object with something more appropriate (lets said return the actual config value of the field), you use the indexed array YAML format :

- debug
- cache
- cache_time

If you also want to specify the default values in the file, you have to use the associative array YAML format :

debug: false
cache: false
cache_time: 300

XXX.form.yml

The form declaration file is more or less the structure you end up with when you declare your form array manually. The only particularities are that you do not use the '#' in front of keys (lazy me), and you declare children of an element in a '_fields' element.

Here is an example :

general:
  type: fieldset
  title: General settings
  _fields:
    debug:
      type: checkbox
      title: 'Debug mode'
      description: 'Enable debug mode to show informations'
    cache:
      type: checkbox
      title: 'Activate cache'
      description: 'Enable caching'
    cache_time:
      type: textfield
      title: 'Cache expiration'
      description: 'Cache expiration in second. 0 or empty is for none (do not use)'
      states:
        visible:
          ':input[name="tools_cache"]': [checked: true]

Project information

  • caution Minimally maintained
    Maintainers monitor issues, but fast responses are not guaranteed.
  • Ecosystem: Atixnet Tools
  • Created by ccrosaz on , updated
  • shield alertThis project is not covered by the security advisory policy.
    Use at your own risk! It may have publicly disclosed vulnerabilities.

Releases