Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

New Validation Features in Drupal 11.3: Sequentially, CompositeConstraintInterface, and AtLeastOneOf Optimization

By admin, 18 November, 2025
New Validation Features in Drupal 11.3: Sequentially, CompositeConstraintInterface, and AtLeastOneOf Optimization

Drupal 11.3 introduces several important improvements to the validation system, making it more flexible and fully compatible with Symfony. In this article, we’ll cover three key updates:

  1. CompositeConstraintInterface – the interface for composite constraints.

  2. Sequentially constraint – sequential validation that shows only the first error.

  3. AtLeastOneOf optimization – switching to the default Symfony validator.

We’ll explain how things worked before, how they work now, and provide practical examples.


1️⃣ CompositeConstraintInterface

How it worked before

In previous Drupal versions, working with composite constraints was limited. The Drupal ConstraintManager primarily handled single constraints, and using multiple nested constraints required extra code to manually create and call them.

What changed

Drupal 11.3 introduced the interface:

 
\Drupal\Core\Validation\CompositeConstraintInterface

Purpose:

  • Allows Drupal to recognize Symfony composite constraints.

  • Any composite constraint class must implement this interface.

  • Requires a static method getCompositeOptionStatic(), which tells Drupal which nested constraints to instantiate.

  • Makes composite constraints easy to use and fully compatible with ConstraintManager.

Example usage

 
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;
use Drupal\Core\Validation\CompositeConstraintInterface;

class MyCompositeConstraint extends Constraint implements CompositeConstraintInterface {
  
  public $constraints;

  public function __construct($options = null) {
    parent::__construct($options);
    $this->constraints = [
      new NotBlank(),
      new Length(['min' => 5]),
    ];
  }

  public static function getCompositeOptionStatic(): string {
    return 'constraints';
  }
}
  • The constraints array contains the nested checks.

  • ConstraintManager automatically applies them.

Now, module developers can easily create complex constraints without manually iterating through nested validators.


2️⃣ Sequentially constraint

How it worked before

Previously, all constraints were validated simultaneously. For example, in a block:

 
block.block.*:
  mapping:
    region:
      type: string
      label: 'Region'
      constraints:
        NotBlank: []
        Callback:
          callback: ['\Drupal\block\Entity\Block', validateRegion]

If a value was both empty and failed the callback, the user would see two errors at once.

What changed

Drupal 11.3 introduces the Sequentially constraint.

Features:

  • Validates multiple constraints in sequence.

  • Stops at the first failing constraint, skipping the rest.

  • Improves UX by showing only one error at a time.

Example YAML usage

 
block.block.*:
  mapping:
    region:
      type: string
      label: 'Region'
      constraints:
        Sequentially:
          - NotBlank: []
          - Callback:
              callback: ['\Drupal\block\Entity\Block', validateRegion]

How it works:

  1. NotBlank is checked first.

  2. If NotBlank fails → validation stops, error is returned.

  3. If NotBlank passes → Callback is checked.

This approach is ideal for fields with multiple constraints where you want the user to see the first error only.


3️⃣ AtLeastOneOf and switching to Symfony

How it worked before

Drupal 11.2 introduced its own validator:

 
AtLeastOneOfConstraintValidator

It ensured that at least one of the given conditions was met.

What changed

In 11.3, Drupal now uses the default Symfony implementation.

  • If you simply use AtLeastOneOf → no changes.

  • If you extended the validator with custom logic → minor adjustments may be needed.

Example usage:

 
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Email;

$constraint = new AtLeastOneOf([
  new NotBlank(),
  new Email(),
]);
  • The value must be either not blank or a valid email.

  • If neither condition is met → an error is thrown.

Drupal now fully relies on Symfony, simplifying maintenance and improving compatibility.


4️⃣ Visual Flow

The validation process can be visualized as:

 
[Input Value] 
    ↓
[Sequentially]
 ├─> NotBlank → error? → stop
 └─> Callback → error? → stop
    ↓
[CompositeConstraintInterface] applies nested constraints
    ↓
[AtLeastOneOf] ensures at least one condition passes

This shows how Drupal 11.3 validation flows with the new system.


5️⃣ Takeaways for Developers

  1. Sequentially – improves UX by showing the first validation error.

  2. CompositeConstraintInterface – makes creating nested constraints seamless and compatible with Symfony.

  3. AtLeastOneOf – now uses Symfony’s standard validator, reducing maintenance overhead.

If you develop custom modules, forms, or constraints, these changes make validation in Drupal more flexible and easier to manage.

 

Practical Guide: Custom Composite Constraints and Sequentially Validation in Drupal 11.3

Drupal 11.3 gives developers powerful tools to create complex form validation using composite constraints and the Sequentially constraint. This guide walks through a real-world example: creating a user registration form with multiple validation rules.


1️⃣ Scenario

Imagine we have a custom registration form with a field called username. Requirements:

  1. Must not be empty.

  2. Must be at least 5 characters long.

  3. Must not contain forbidden words (admin, root).

  4. If all above are valid, must check a custom callback for uniqueness.

We want Sequentially validation to show only the first error a user encounters.


2️⃣ Create a Composite Constraint

Step 1: Define the custom composite constraint

 
namespace Drupal\custom_module\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;
use Drupal\Core\Validation\CompositeConstraintInterface;

/**
 * Validates a username with multiple rules.
 *
 * @Constraint(
 *   id = "UsernameComposite",
 *   label = @Translation("Username Composite Constraint"),
 * )
 */
class UsernameComposite extends Constraint implements CompositeConstraintInterface {

  public $constraints;

  public function __construct($options = null) {
    parent::__construct($options);
    $this->constraints = [
      new NotBlank(),
      new Length(['min' => 5]),
      new ForbiddenWords(['words' => ['admin', 'root']]),
      new Callback(['callback' => [self::class, 'validateUniqueUsername']]),
    ];
  }

  public static function getCompositeOptionStatic(): string {
    return 'constraints';
  }

  public static function validateUniqueUsername($value, $context) {
    // Example uniqueness check.
    if ($value === 'existinguser') {
      $context->buildViolation('This username is already taken.')->addViolation();
    }
  }
}

Notes:

  • ForbiddenWords is a custom constraint checking for forbidden words.

  • Callback runs the uniqueness check only if the first three validations pass.

  • Implementing CompositeConstraintInterface allows ConstraintManager to handle all nested constraints automatically.


Step 2: Apply Sequentially in the form

 
use Symfony\Component\Validator\Constraints\Sequentially;
use Drupal\custom_module\Plugin\Validation\Constraint\UsernameComposite;

$form['username'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Username'),
  '#constraints' => [
    new Sequentially([
      new UsernameComposite()
    ]),
  ],
];
  • Sequentially ensures the first failing rule stops validation.

  • Users see one error at a time instead of a long list.


3️⃣ Example Input and Output

Input ValueError Displayed
(empty)"This value should not be blank." (NotBlank)
"adm""This value is too short. Minimum 5 characters." (Length)
"admin123""This word is forbidden." (ForbiddenWords)
"existinguser""This username is already taken." (Callback)
"validuser"No errors, passes validation

Notice how Sequentially stops at the first failing constraint and does not display later errors.


4️⃣ Benefits of This Approach

  1. Cleaner UX: Users are not overwhelmed by multiple errors at once.

  2. Reusability: Composite constraints can be reused across multiple forms.

  3. Compatibility: Fully compatible with Symfony validation and Drupal’s ConstraintManager.

  4. Flexible: You can mix built-in constraints (NotBlank, Length) with custom logic (ForbiddenWords, Callback).


5️⃣ Tips for Developers

  • Always implement CompositeConstraintInterface when creating multi-rule constraints.

  • Use Sequentially for critical fields where showing the first error is enough.

  • Combine with AtLeastOneOf for conditional validations, e.g., “must be a number or valid email.”

  • Keep custom callbacks light; heavy operations can slow down validation.


This setup ensures your forms are robust, user-friendly, and maintainable, taking full advantage of Drupal 11.3’s new validation features.

 

Here’s a full demo Drupal 11.3 module implementing the UsernameComposite constraint with Sequentially validation. You can drop it into modules/custom/username_validation_demo and test it immediately.


1️⃣ Module Structure

 
username_validation_demo/
├─ username_validation_demo.info.yml
├─ src/
│  └─ Plugin/
│     └─ Validation/
│        └─ Constraint/
│           ├─ UsernameComposite.php
│           └─ ForbiddenWords.php
└─ src/Form/UsernameDemoForm.php 

2️⃣ username_validation_demo.info.yml

 
name: 'Username Validation Demo' 
type: module 
description: 'Demo module for Sequentially and CompositeConstraint validation in Drupal 11.3' 
core_version_requirement: ^11 
package: Custom 
dependencies: 
  - drupal:system 

3️⃣ Composite Constraint: UsernameComposite.php

 
<?php

namespace Drupal\username_validation_demo\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Callback;
use Drupal\Core\Validation\CompositeConstraintInterface;

/
 * Validates a username using multiple rules.
 *
 * @Constraint(
 *   id = "UsernameComposite",
 *   label = @Translation("Username Composite Constraint")
 * )
 */
class UsernameComposite extends Constraint implements CompositeConstraintInterface {

  public $constraints;

  public function __construct($options = null) {
    parent::__construct($options);

    $this->constraints = [
      new NotBlank(),
      new Length(['min' => 5]),
      new ForbiddenWords(['words' => ['admin', 'root']]),
      new Callback(['callback' => [self::class, 'validateUniqueUsername']]),
    ];
  }

  public static function getCompositeOptionStatic(): string {
    return 'constraints';
  }

  public static function validateUniqueUsername($value, $context) {
    // Example uniqueness check.
    if ($value === 'existinguser') {
      $context->buildViolation('This username is already taken.')->addViolation();
    }
  }
}

4️⃣ ForbiddenWords Constraint: ForbiddenWords.php

 
<?php

namespace Drupal\username_validation_demo\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/
 * Checks that value does not contain forbidden words.
 *
 * @Constraint(
 *   id = "ForbiddenWords",
 *   label = @Translation("Forbidden Words Constraint")
 * )
 */
class ForbiddenWords extends Constraint {

  public $words = [];

  public $message = 'This word is forbidden.';

  public function __construct($options = null) {
    parent::__construct($options);
    if (!empty($options['words'])) {
      $this->words = $options['words'];
    }
  }
}

class ForbiddenWordsValidator extends ConstraintValidator {

  public function validate($value, Constraint $constraint) {
    foreach ($constraint->words as $word) {
      if (stripos($value, $word) !== FALSE) {
        $this->context->buildViolation($constraint->message)->addViolation();
        return; // Stop at first match
      }
    }
  }
}

5️⃣ Demo Form: UsernameDemoForm.php

 
<?php

namespace Drupal\username_validation_demo\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Validator\Constraints\Sequentially;
use Drupal\username_validation_demo\Plugin\Validation\Constraint\UsernameComposite;

class UsernameDemoForm extends FormBase {

  public function getFormId() {
    return 'username_demo_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['username'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Username'),
      '#constraints' => [
        new Sequentially([
          new UsernameComposite()
        ]),
      ],
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addMessage($this->t('Username is valid: @username', ['@username' => $form_state->getValue('username')]));
  }
}

✅ How it works

  • The UsernameComposite constraint applies NotBlank → Length → ForbiddenWords → Callback in order.

  • Wrapped in Sequentially, it stops at the first failing rule.

  • Users see one error at a time, improving UX.

  • Valid usernames pass all checks and display a success message.


6️⃣ Testing

Input ValueError Displayed
(empty)"This value should not be blank." (NotBlank)
"adm""This value is too short. Minimum 5 characters." (Length)
"admin123""This word is forbidden." (ForbiddenWords)
"existinguser""This username is already taken." (Callback)
"validuser"Success message

This demo module gives a complete working example of Sequentially validation and composite constraints in Drupal 11.3.

 

Let’s enhance your Drupal 11.3 demo module with real-time AJAX validation so users see the first failing rule immediately as they type.


1️⃣ Overview

We’ll make the username field validate via AJAX:

  • Uses Sequentially + UsernameComposite constraints.

  • Sends the current input to the server on keyup or blur.

  • Returns the first validation error immediately without submitting the form.


2️⃣ Update the Form

Edit UsernameDemoForm.php:

 
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;

class UsernameDemoForm extends FormBase {

  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['username'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Username'),
      '#constraints' => [
        new Sequentially([
          new UsernameComposite()
        ]),
      ],
      '#ajax' => [
        'callback' => '::validateUsernameAjax',
        'event' => 'keyup',
        'wrapper' => 'username-error-wrapper',
        'progress' => [
          'type' => 'throbber',
          'message' => NULL,
        ],
      ],
    ];

    $form['username_error'] = [
      '#type' => 'markup',
      '#markup' => '',
      '#prefix' => '<div id="username-error-wrapper">',
      '#suffix' => '</div>',
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  public function validateUsernameAjax(array &$form, FormStateInterface $form_state) {
    $value = $form_state->getValue('username');

    // Manually validate the Sequentially constraint.
    $constraint = new Sequentially([
      new UsernameComposite(),
    ]);

    $validator = \Drupal::service('validator');
    $violations = $validator->validate($value, $constraint);

    $response = new AjaxResponse();

    if (count($violations) > 0) {
      $firstViolation = $violations[0]->getMessage();
      $response->addCommand(new HtmlCommand('#username-error-wrapper', $firstViolation));
    }
    else {
      $response->addCommand(new HtmlCommand('#username-error-wrapper', ''));
    }

    return $response;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addMessage($this->t('Username is valid: @username', ['@username' => $form_state->getValue('username')]));
  }
}

3️⃣ How It Works

  1. The #ajax property is attached to the username field.

    • Event: keyup → triggers validation after each keystroke.

    • Wrapper: #username-error-wrapper → the element where the error message appears.

  2. In validateUsernameAjax():

    • We manually validate the value against Sequentially([UsernameComposite]).

    • Take the first violation (if any) and return it as HTML.

    • If there’s no violation, the error message is cleared.

  3. The user sees real-time feedback as they type, stopping at the first failing constraint, improving UX.


4️⃣ Testing Examples

Input ValueAJAX Error Displayed
(empty)"This value should not be blank."
"adm""This value is too short. Minimum 5 characters."
"admin123""This word is forbidden."
"existinguser""This username is already taken."
"validuser"No error → field is valid

✅ Result:
The module now validates username in real-time, shows only the first error using Sequentially, and gives instant feedback without form submission.

Tags

  • Drupal
  • Validation Features
  • Composite Constraints
  • Sequentially Constraint
  • AtLeastOneOf
  • Symfony Validator
  • Custom Validation

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal