Config validation

Drupal's next leap?


Wim Leers
Senior Principal Software Engineer
Acquia logo's Drupal Acceleration Team

What's this about really? ๐Ÿ˜…


icon_base_uri: 'public://media-icons/generic'
iframe_domain: ''
oembed_providers_url: 'https://oembed.com/providers.json'
standalone_url: false
media.settings.yml

What's this about really? ๐Ÿ˜…


_core:
  default_config_hash: PlWtVQXY5oKYZqCMPXh6SPamXagn5BoZqgAI8EY9WsY
icon_base_uri: 'public://media-icons/generic'
iframe_domain: ''
oembed_providers_url: 'https://oembed.com/providers.json'
standalone_url: false
media.settings.yml when installed
A brief history of config in Drupal
erastorageblobbiness
2004variable tableextremely๐Ÿ™ˆ
2012YAML filesdiffable๐Ÿค“๐”
2013[โ€ฆ] + schemaintrospectable๐Ÿค”๐Ÿ’ก
2014config table + [โ€ฆ]unchanged๐ŸŽ๏ธ
2017[โ€ฆ] + [โ€ฆ] + constraintsvalidatableโœ…

Trend: more structure

  • introspectable  (๐Ÿ‘‹ variable module)
  • translatable (๐Ÿ‘‹ i18n module)
  • exportable    (๐Ÿ‘‹ ctools exportables)

type: uuid

๐Ÿ‘†

the only validatable config type

๐Ÿ˜ฑ

Config schema 101

๐Ÿ“š

type: uuid use

config_entity:
  type: mapping
  mapping:
    uuid:
      type: uuid
      label: 'UUID'
  โ€ฆ

type: uuid definition

# A UUID.
uuid:
  type: string
  label: 'UUID'
  constraints:
    Uuid: {}
core/config/schema/core.data_types.schema.yml
"Uuid" ๐Ÿ‘‰ a @Constraint plugin ID
namespace Drupal\Core\Validation\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraints\Uuid;
use Symfony\Component\Validator\Constraints\UuidValidator;

/**
 * Validates a UUID.
 *
 * @Constraint(
 *   id = "Uuid",
 *   label = @Translation("Universally Unique Identifier", context = "Validation"),
 * )
 */
class UuidConstraint extends Uuid {

  /**
   * {@inheritdoc}
   */
  public function validatedBy() {
    return UuidValidator::class;
  }

  public $message = 'This is not a valid UUID.';

}
namespace Symfony\Component\Validator\Constraints;

class UuidValidator extends ConstraintValidator {

    public function validate(mixed $value, Constraint $constraint) {
        if (!$constraint instanceof Uuid) {
            throw new UnexpectedTypeException($constraint, Uuid::class);
        }
        if (!\is_scalar($value) && !$value instanceof \Stringable) {
            throw new UnexpectedValueException($value, 'string');
        }
        โ€ฆ
    }

}

๐Ÿง‘โ€๐ŸŽ“๐Ÿฅณ

Before type: uuid

     uuid:
-      type: string
-      label: 'UUID'
+      type: uuid
Any string ๐Ÿ˜ณ

๐Ÿค”

๐Ÿคฏ

media.settings

DataTypes
ConfigStoragePerceived
'f962b8c7- 4c74-4100- b6de-08e6a65ff43d'uuidstringUUID๐Ÿ‘
'https://oembed.com/ providers.json'uristringURL๐Ÿ‘
''uristring๐Ÿคช๐Ÿ™ˆ
intentshapesemantics

Validators needed! ๐Ÿ™

It gets worse.

media.settings

DataTypes
ConfigStoragePerceived
'f962b8c7- 4c74-4100- b6de-08e6a65ff43d'uuidstringUUID๐Ÿ‘
'https://oembed.com/ providers.json'uristringURL๐Ÿ‘
''uristring๐Ÿคช๐Ÿ™ˆ
'first-uuid'uuidstring๐Ÿคช๐Ÿ˜ฑ

Validators are not executed today.

The new blobbiness

๐Ÿซ 

  • Many validators missing.
  • Validators not executed.

Why?

๐Ÿคทโ€โ™€๏ธ

  • Writing configuration via JSON:API
  • Admin UI improvements ๐Ÿชฆ JS Admin UI
  • Reliable config_split, config_filter โ€ฆ
  • Recipes Initiative
  • Automatic Updates Initiative
  • Ambitious site builder experiences

Drupal Core

Ugly

>95% of config types not validatable

#3324984 ๐Ÿ‘‰ test to track this %

Bad

Every test checks schema โ€ฆ

โ€ฆ but does not validate!

#3361534 ๐Ÿ‘‰ validate()

Good

More config validatable!

  • CKEditor 5 configuration + every plugin!
  • type: config_dependencies
  • block.block.*:plugin
  • #2920678 ๐Ÿ‘‰ type: machine_name
  • #3341682 ๐Ÿ‘‰ type: label

Good

More validation constraints to reuse!

  • ValidKeys: <infer>
  • PluginExists: plugin.manager.block
  • ExtensionName: {}
  • ExtensionExists: module
  • ConfigExists: {}
  • โ€ฆ

Interested?

Issue tags:

Configuration Inspector

d.o/project/config_inspector

Since 2.1.1

Drupal Contrib

You can start using this today!

CDN module: since 2019

  1. Copy ValidatableConfigFormBase which extends \Drupal\Core\Form\ConfigFormBase
  2. Subclass ValidatableConfigFormBase instead of ConfigFormBase
  3. ๐Ÿ’ฐ๐ŸŒˆ
  1. Copy ValidatableConfigFormBase which extends \Drupal\Core\Form\ConfigFormBase
  2. Subclass ValidatableConfigFormBase instead of ConfigFormBase
  3. ๐Ÿ’ฐ๐ŸŒˆ
  4. Optional: ConfigEvents::SAVE subscriber to validate your config
  5. ๐Ÿฅณ๐Ÿฅ‡

Examples

  • Search for constraints
  • in *.schema.yml files
๐Ÿ‘‡

How to adopt?

๐Ÿชฉ

Analyze your entire site

drush config:inspect --format=csv > ~/Desktop/core.csv

$ drush config:inspect --filter-keys=user.settings --detail --list-constraints
 Legend for Data:
  โœ…โ“  โ†’ Correct primitive type, detailed validation impossible.
  โœ…โœ…  โ†’ Correct primitive type, passed all validation constraints.
 ------------------------------------------ --------- ------------- ------ ---------------------------------
  Key                                        Status    Validatable   Data   Validation constraints
 ------------------------------------------ --------- ------------- ------ ---------------------------------
  user.settings                              Correct   74%           โœ…โ“
   user.settings:                            Correct   NOT           โœ…โ“
   user.settings:_core                       Correct   NOT           โœ…โ“
   user.settings:_core.default_config_hash   Correct   NOT           โœ…โ“
   user.settings:anonymous                   Correct   NOT           โœ…โ“
   user.settings:cancel_method               Correct   NOT           โœ…โ“
   user.settings:langcode                    Correct   NOT           โœ…โ“
   user.settings:notify                      Correct   NOT           โœ…โ“
   user.settings:notify.cancel_confirm       Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
   user.settings:notify.password_reset       Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
โ€ฆ
   user.settings:notify.status_activated     Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
   user.settings:notify.status_blocked       Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
   user.settings:notify.status_canceled      Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
   user.settings:password_reset_timeout      Correct   NOT           โœ…โ“
   user.settings:password_strength           Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
   user.settings:register                    Correct   NOT           โœ…โ“
   user.settings:verify_mail                 Correct   Validatable   โœ…โœ…   โ†ฃ PrimitiveType: {  }
 ------------------------------------------ --------- ------------- ------ ------------------------
          

  user.settings:
    type: config_object
    label: 'User settings'
    mapping:
      anonymous:
        type: label
        label: 'Name'
+       constraints:
+         NotBlank: []
+         Regex:
+           pattern: '/[[:alnum:]]+/'
+           message: 'Using only emojis is not allowed because it may not be supported in all contexts.'
          

user.schema.yml


-class AccountSettingsForm extends ConfigFormBase {
+class AccountSettingsForm extends ValidatableConfigFormBase {
          

   public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);

-    $this->config('user.settings')
-      ->set('anonymous', $form_state->getValue('anonymous'))
-      ->set('register', $form_state->getValue('user_register'))
-      ->set('password_strength', $form_state->getValue('user_password_strength'))
-      ->set('verify_mail', $form_state->getValue('user_email_verification'))
-      ->set('cancel_method', $form_state->getValue('user_cancel_method'))
-      ->set('notify.status_activated', $form_state->getValue('user_mail_status_activated_notify'))
-      ->set('notify.status_blocked', $form_state->getValue('user_mail_status_blocked_notify'))
-      ->set('notify.status_canceled', $form_state->getValue('user_mail_status_canceled_notify'))
-      ->save();

+  /**
+   * {@inheritdoc}
+   */
+  protected static function mapFormValuesToConfig(FormStateInterface $form_state, Config $config): Config {
+    switch ($config->getName()) {
+      case 'user.settings':
+        $config->set('anonymous', $form_state->getValue('anonymous'))
+          ->set('register', $form_state->getValue('user_register'))
+          ->set('password_strength', $form_state->getValue('user_password_strength'))
+          ->set('verify_mail', $form_state->getValue('user_email_verification'))
+          ->set('cancel_method', $form_state->getValue('user_cancel_method'))
+          ->set('notify.status_activated', $form_state->getValue('user_mail_status_activated_notify'))
+          ->set('notify.status_blocked', $form_state->getValue('user_mail_status_blocked_notify'))
+          ->set('notify.status_canceled', $form_state->getValue('user_mail_status_canceled_notify'));
+    }
+    return $config;
+  }

๐Ÿ•บ

#3364506

๐Ÿ™

๐Ÿš€

  • Challenge: partially validatable
  • Functionality: JS admin UI, JSON:API, recipes
  • Operational: reliability (CI!), module maintainability

๐Ÿ™‡๐Ÿปโ€โ™‚๏ธ

META#2164373
๐Ÿ‘€  โคต
Required values#3364109
Required keys#3364108
Validate in tests#3361534
Metadata for rich UX#2949888
Which config changes have caused outages for YOU? ๐Ÿซต