Starting in Drupal 11.3.x, validation constraints (Constraint plugins) — which are wrappers around Symfony Validator — are transitioning to a new way of passing parameters: named arguments.
This is part of a broader effort to prepare for Symfony 8, which will be included in Drupal 12.
In this article, we’ll look at what is changing, why it matters, and how you can update your custom constraint plugins safely.
🔍 Background
Previously, constraint plugins received all configuration options via a single associative array:
$constraint = new MyConstraint(['message' => 'Custom validation message']);
This worked for years but had several downsides:
- No strict type checking
- Hard to clearly separate required/optional arguments
- Poor IDE auto-completion
- Better validation only happens at runtime
Symfony is improving its API by replacing this array-based approach with strongly typed named arguments.
✅ What’s Changing?
The new recommended format is:
$constraint = new MyConstraint(message: 'Custom validation message');
This provides:
✔ Better type safety
✔ Clearer API
✔ IDE auto-completion
✔ Improved developer experience (DX)
The legacy syntax using arrays is deprecated in Drupal 11.3
and will be removed in Drupal 12.
🔧 Updating Your Constraint Plugin
To support named arguments, you must:
- Add your own
__construct() - Mark it with
#[HasNamedArguments] - Expose configuration parameters as constructor arguments
Example — new format
use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
class MyConstraint extends Constraint {
#[HasNamedArguments]
public function __construct(
public string $message = 'Default validation message.',
mixed ...$args,
) {
parent::__construct(...$args);
}
}
Thanks to constructor property promotion, $message is automatically stored as a class property.
🔄 Backward compatibility (optional)
If you need to support both new and old syntax, you can write a hybrid constructor:
use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
class MyConstraint extends Constraint {
public string $message = 'Default validation message.';
#[HasNamedArguments]
public function __construct(?array $options = null, ?string $message = null, mixed ...$args) {
if (is_array($options)) {
@trigger_error(sprintf(
'Passing an array of options to configure the "%s" constraint is deprecated in drupal:11.3.0 and removed in drupal:12.0.0. Use named arguments instead.',
static::class
), E_USER_DEPRECATED);
}
parent::__construct($options, ...$args);
$this->message = $message ?? $this->message;
}
}
This:
- Supports legacy array-based configuration
- Logs a deprecation warning
- Prepares code for Drupal 12
🚨 Deprecated Usage
❌ Old — deprecated in Drupal 11.3, removed in Drupal 12:
$constraint = new MyConstraint(['message' => 'Error']);
✅ Correct Usage
$constraint = new MyConstraint(message: 'Error');
⚡ Who Is Affected?
✅ Affected
✔ Custom module developers
✔ Contrib module maintainers
✔ Teams implementing custom constraints
❌ Not affected
✘ Editors
✘ End users
💡 Recommendations
✔ Start using named arguments now
✔ Update your constraint constructors
✔ Add #[HasNamedArguments]
✔ Use typed parameters
✔ Provide backward compatibility if needed
📦 Summary
Drupal is modernizing its API surface by migrating constraint plugin configuration from array-based definitions to typed, named constructor arguments.
This change:
✅ Improves type safety
✅ Makes API usage more explicit
✅ Enables better IDE support
✅ Simplifies refactoring
✅ Enhances developer experience
Because Symfony 8 will remove support for the old format, updating now ensures compatibility with Drupal 12 and beyond.
Great — here is a realistic before/after example of updating a contrib-style constraint.
(This is an illustrative example modeled after real-world Drupal contrib modules.)
✅ Example Migration in a Contrib Module
Let’s imagine a contrib module defines a constraint calledCheckKeywordsConstraint.
It previously accepted configuration like:
$constraint = new CheckKeywordsConstraint([
'minWords' => 3,
'message' => 'Not enough keywords provided.',
]);
❌ Before (deprecated)
<?php
namespace Drupal\example\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/
* Checks that the text contains enough keywords.
*
* @Constraint(
* id = "CheckKeywords",
* label = @Translation("Keyword count check")
* )
*/
class CheckKeywordsConstraint extends Constraint {
public $minWords = 1;
public $message = 'Not enough keywords found.';
// No custom constructor → parent handled options array.
}
This worked fine when constraints could accept an options array, but
with Drupal 11.3 this becomes deprecated, and will break in Drupal 12.
✅ After — Updated for Named Arguments
✅ Supports new syntax
✅ Strong typing
✅ Supports backwards compatibility (optional but recommended)
<?php namespace Drupal\example\Plugin\Validation\Constraint; use Symfony\Component\Validator\Attribute\HasNamedArguments; use Symfony\Component\Validator\Constraint; /* Checks that the text contains enough keywords.
*
* @Constraint(
* id = "CheckKeywords",
* label = @Translation("Keyword count check")
* )
*/
class CheckKeywordsConstraint extends Constraint {
public int $minWords = 1;
public string $message = 'Not enough keywords found.';
#[HasNamedArguments]
public function __construct(
?array $options = null,
?int $minWords = null,
?string $message = null,
mixed ...$args,
) {
if (is_array($options)) {
@trigger_error(sprintf(
'Passing an array of options to configure "%s" is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. Use named arguments instead.',
static::class
), E_USER_DEPRECATED);
}
parent::__construct($options, ...$args);
// Assign properties.
$this->minWords = $minWords ?? $this->minWords;
$this->message = $message ?? $this->message;
}
}
✅ New Usage
$constraint = new CheckKeywordsConstraint(
minWords: 3,
message: 'You need at least 3 keywords.'
);
With named arguments, validation configuration becomes cleaner and type-safe.
Side-by-side Summary
| Aspect | Before | After |
|---|---|---|
| Pass configuration | Associative array | Named arguments |
| Constructor needed | No | Yes |
| Type safety | Weak | Strong |
| IDE autocomplete | Poor | Good |
| Deprecation status | Deprecated in 11.3 | Compatible |
| Drupal 12 support | ❌ No | ✅ Yes |
Optional: Simplified version (no BC)
✅ If you don’t need to support old array format:
class CheckKeywordsConstraint extends Constraint {
#[HasNamedArguments]
public function __construct(
public int $minWords = 1,
public string $message = 'Not enough keywords found.',
mixed ...$args,
) {
parent::__construct(...$args);
}
}
Cleaner — but no BC for older usage.
⚡ Validator Example (for completeness)
Validator code does not change:
<?php
namespace Drupal\example\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CheckKeywordsConstraintValidator extends ConstraintValidator {
public function validate($value, Constraint $constraint) {
if (!$value) {
return;
}
$word_count = str_word_count($value);
if ($word_count < $constraint->minWords) {
$this->context->addViolation($constraint->message);
}
}
}
✅ Practical Notes for Contrib Maintainers
✔ Always use typed constructor arguments
✔ Use #[HasNamedArguments]
✔ Decide whether you need BC
✔ Trigger deprecation when array config is used
✔ Update docs + example code
Comments