HomeServiceContact
Drupal
min read
July 3, 2024

Exploring the new "#config_target" option in Drupal 10

Exploring the new "#config_target" option in Drupal 10
Table of contents

Have you ever created a configuration form? It is one of the first things that we learn as Drupal developers. We use configuration forms to store values in configuration and then build functionalities based on those stored values. A good example is the ‘Basic site settings’ where we add the site name, site email, front page etc.

What is configuration validation? 

Let's go back to the ‘Basic site settings’. Go to the ‘Error pages’ section, add ‘/i-love-drupal’ as the ‘Default 404 (not found) page’ and try to save the configuration.

configuration validation

You will get an error message saying that the path is not valid. Now create a page in your site with the alias ‘/i-love-drupal’ and try to save this configuration again.

Path is not valid error message

You will be able to save the configuration now. What happened here? Drupal expects a valid URL as the ‘default 404 page URL’. So the user input is checked before the values are saved to the configuration to ensure that the value given is a valid URL. If it’s not, the configuration is not updated and an error message is displayed to the user.

Usually, such validations are added from the form class using the validateForm() method.



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

    // Validate 404 error path.
    if (!$form_state->isValueEmpty('site_404') && !$this->pathValidator->isValid($form_state->getValue('site_404'))) {
      $form_state->setErrorByName('site_404', $this->t("Either the path '%path' is invalid or you do not have access to it.", ['%path' => $form_state->getValue('site_404')]));
    }

    parent::validateForm($form, $form_state);
  }




But now, we can also add such validations from the schema.yml file by using constraints, without using validateForm() method. A new property ‘#config_tagret’ was introduced in Drupal 10.2 to support such config validations. 

How does it work?

  • Create a simple configuration form with one text field. Keep it as a non-mandatory field.
  • Values submitted by the user would be stored in my_custom_module.settings.yml.


 /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name’'),
    ];
    return parent::buildForm($form, $form_state);
  }



  • Now assume that when a user adds a value to the ‘name’ field, it should be stored under the ‘username’ key in my_custom_module.settings.yml. Traditionally, we use $config->set(‘username’, $form_state->getValue(‘name’)) In submitForm() method to do this. Now we can do this directly from the buildForm() method itself, by using the new ‘#config_target’ option.

  • Create a schema file for this configuration.


my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string # Indicates that the value stored in this field will be string.
      label: 'Username’'



  • Add ‘#config_target’ property to the ‘name’ form element.


  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
      '#config_target' => 'my_custom_module.settings:username',
    ];
    return parent::buildForm($form, $form_state);
  }



  • '#config_target' => 'my_custom_module.settings:username' tells the form to store the value from the field under the ‘username’ key in my_custom_module.settings configuration.
  • Now, open the form. Add a name and submit it.

Saved status message
  • You will see that the value has been submitted.
  • Export the configuration and check my_custom_module.settings.yml. The submitted value will be stored under the ‘username’ key.
Value submission for configuration
  • The field even renders the submitted value as the default value, even though we have not explicitly provided the ‘#default_value’ property. We haven’t even added the validateForm() and submitForm() methods. Isn’t that cool?

This looks great. But how to validate the user input?

  • Validations are added using the constraints key in the schema.yml file.
  • We can use the NotBlank constraint to make the name field a mandatory field.


# Schema for the configuration files of the My custom module module.
my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string
      label: 'Username'
      constraints:
        NotBlank: []



  • Now try to save the form without adding any value in the name field. The form will throw an error
Error status message for blank field
  • We could even customize the error message.


my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string
      label: 'Username'
      constraints:
        NotBlank:
          message: "This field is inevitable."



Field inevitable error message
  • Let's add one more text field ‘purpose’ to the form. The value from this field should be stored under the ‘purpose’ key in the ‘my_custom_module.settings’ config. So the config target will be ‘my_custom_module.settings:purpose’.


 public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
      '#config_target' => 'my_custom_module.settings:username',
    ];
    $form['purpose'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Why do you need the infinity stones?'),
      '#config_target' => 'my_custom_module.settings:purpose',
    ];
    return parent::buildForm($form, $form_state);
  }



  • Update the schema file and include data corresponding to this field. This time, let's add another validation constraint Choice to restrict the values to a specific set.


# Schema for the configuration files of the My custom module module.
my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string
      label: 'Username'
      constraints:
        NotBlank:
          message: "This field is inevitable."
    purpose:
      type: string
      label: 'Purpose'
      constraints:
        Choice:
          - To wipe out half of the universe.



  • Now, if you try to save the ‘purpose’ field with an invalid value, you will get an error.
Not valid choice error message
  • There are many more types of constraints available such as Regex, NotNull, email, Length, etc. Multiple constraints can be added to validate a single field as well.

Eg:



  id:
      type: machine_name
      label: 'ID'
      # Menu IDs are specifically limited to 32 characters, and allow dashes but not
      # underscores.
      # @see \Drupal\menu_ui\MenuForm::form()
      constraints:
        Regex:
          pattern: '/^[a-z0-9-]+$/'
          message: "The %value machine name is not valid."
        Length:
          max: 32



  • Commonly used constraints could be found in web/core/config/schema/core.data_types.schema.yml. We could also create our own constraints which we will explore towards the end of this blog.

Transforming config values using ‘#config_target’

  • As we saw earlier, when '#config_target' => 'my_custom_module.settings:username' is added to a form field, it takes the user input and saves it under the ‘username’ key in ‘my_custom_module.settings’.
  • But what if we want to transform the value before saving it in the config and vice versa? Let's check with an example.
  • Add a checkbox field ‘future_plan’ and map it to the ‘future_plan’ key in the configuration.


public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Your name'),
      '#config_target' => 'my_custom_module.settings:username',
    ];
    $form['purpose'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Why do you need the infinity stones?'),
      '#config_target' => 'my_custom_module.settings:purpose',
    ];
    $form['future_plan'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Would you still go to work after using the infinity stones?'),
      '#config_target' => 'my_custom_module.settings:future_plan',
    ];
    return parent::buildForm($form, $form_state);
  }



  • By default, when the user submits the form, either 1 or 0 would get saved in the configuration. We could also change it to ‘TRUE’ or ‘FALSE’ by changing the type to ‘boolean’ in the schema file.
  • But what if we want to store something else in the config? For example, what if we need to store ‘YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE’ when the user checks the checkbox and ‘NO! I WILL DESTROY THE STONES, RETIRE, AND BECOME A FARMER’ if the user doesn’t.
  • To do such transformations, we could create a ConfigTarget object and assign it as the value of ‘#config_target’.
  • The ConfigTarget constructor accepts 4 arguments.
    • Config name (Required): The name of the config object being read from or written to, e.g. `my_custom_module.settings`.
    • Property path (Required): The property path(s) being read or written, e.g., `username`
    • fromConfig (Optional): A callback that transforms the value stored in the configuration before it gets displayed in the form.
    • toConfig (Optional): A callback that transforms the user input in a field before it gets saved in the config.
  • Lets us create the ConfigTarget object for our checkbox field. First let's create the callbacks
  • If the user checks the checkbox, we need to store ‘YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE’. Otherwise, we need to store ‘NO! I WILL DESTROY THE STONES, RETIRE, AND BECOME A FARMER’. So our toConfg callback can be written as


fn ($value) => $value ? 'YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE' : 'NO! I WILL DESTROY THE STONES, RETIRE, AND BECOME A FARMER',



  • Similarly,if the value present in the config is ‘YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE’, the checkbox should be checked when the user views the form. Else checkbox should be unchecked. So most simply, our fromConfig callback can be written as


 fn ($value) => ($value == 'YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE'),


  • So, the ‘future_plan’ field can be rewritten as:


  $form['future_plan'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Would you still go to work after using the infinity stones?'),
      '#config_target' => new ConfigTarget(
        'my_custom_module.settings',
        'future_plan',
        // Converts config value to a form value.
        fn ($value) => ($value == 'YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE'),
        // Converts form value to a config value.
        fn ($value) => $value ? 'YES! I WILL CONTINUE TO EXPLORE THE UNIVERSE' : 'NO! I WILL DESTROY THE STONES, RETIRE, AND BECOME A FARMER',
      )
    ];



  • We need to update the schema file as well. Let us keep the type as ‘string’ since we are saving a string in config.


# Schema for the configuration files of the My custom module module.
my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string
      label: 'Username'
      constraints:
        NotBlank:
          message: "This field is inevitable."
    purpose:
      type: string
      label: 'Purpose'
      constraints:
        Choice:
          - To wipe out half of the universe.
    future_plan:
      type: string
      label: 'Future plan'



  • Load the form again and try updating the checkbox. You will see the ‘future_plan’ key getting updated in config depending upon the state of the checkbox and vice versa.
Configuration checkbox message

Adding a custom validation constraint

  • Adding a custom constraint involves 2 steps.
    • Creating a validation constraint.
    • Assigning the validation constraint to the field in the schema file
  • Add  a radio field to our form.


 $form['save_ironman'] = [
      '#title' => t("Doctor Strange has agreed to give you the Time Stone if you don't hurt Iron Man. What do you think?"),
      '#type' => 'radios',
      '#options' => [
        'Yes' => "It's a great deal!",
        'No' => "Seems suspicious!",
      ],
      '#config_target' => 'my_custom_module.settings:save_ironman'
    ];



   Since everyone loves Iron Man, let's add a custom validation constraint to force the user to select ‘Yes’ always.

  • First, create SaveIronmanConstraint in my_custom_module/src/Plugin/Validation/Constraint/SaveIronmanConstraint.php and add the error message there.


namespace Drupal\my_custom_module\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Provides a Save Ironman constraint.
 *
 * @Constraint(
 *   id = "SaveIronman",
 *   label = @Translation("Save Ironman", context = "Validation"),
 * )
 */
final class SaveIronmanConstraint extends Constraint {

  public string $message = 'Accept the deal. If Iron man wants to stop me, he should build a time machine which is IMPOSSIBLE.';

}



  • Then, create the ‘SaveIronmanConstraintValidator’ in my_custom_module/src/Plugin/Validation/Constraint/SaveIronmanConstraintValidator.php


/**
 * Validates the Save Ironman constraint.
 */
final class SaveIronmanConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate(mixed $value, Constraint $constraint): void {
    // Display error when the value is not ‘Yes’.
    if ($value != 'Yes') {
      $this->context->addViolation($constraint->message);
    }
  }

}



  • Finally,assign the id of our custom constraint to the ‘save_ironman’ field in the schema file.


my_custom_module.settings:
  type: config_object
  label: 'My custom module settings'
  mapping:
    username:
      type: string
      label: 'Username'
      constraints:
        NotBlank:
          message: "This field is inevitable."
    purpose:
      type: string
      label: 'Purpose'
      constraints:
        Choice:
          - To wipe out half of the universe.
    future_plan:
      type: string
      label: 'Future plan'
    save_ironman:
      type: string
      label: 'Save Ironman'
      constraints:
        SaveIronman: [ ]



  • Load the form now and try to save it by selecting ‘Seems suspicious’.
Error status message

You might feel skeptical about using the ‘#config_target’ property in config forms for multiple reasons. One of the reasons could be that when we add validations at the form level, all the logic would be under the same form class, which is convenient in most cases. However, validations added at the form level would only work when the user saves the form. In contrast, the new config validation would work in other scenarios, such as when applying recipes to the site, when saving configurations programmatically, etc. Many forms in Drupal core have already adopted the ‘#config_target’ approach to validation. It's an integral part of many ongoing and upcoming Drupal initiatives as well. So, adopting it is definitely a step forward that will keep our module ready for future Drupal releases.

A quick recap of all the things we explored in this blog

  1. The new ‘#config_target’ property and how to use it.
  2. Commonly used validation constraints (NotBlank, Choice, Regex etc.)
  3. Transforming the form values before saving to configuration and vice versa.
  4. Creating custom validation constraints.
  5. Advantages of the new configuration validation.

Github URL

Written by
Editor
No art workers.
We'd love to talk about your business objectives