Writing a custom Extbase validator
When the built-in validators cannot
express a domain rule — for example "a conference end date must be after its
start date" or "registration is only open while seats remain" — write a
custom validator class and attach it with
# just like any
built-in validator.
Model property vs. action parameter — where you place the validator determines what it can see and when it runs.
A validator on a model property receives only that property's value. It
runs on every action that takes the model as an argument. Use it for
self-contained rules that must always hold: $title must not be empty,
$contactEmail must be a valid address.
A validator on an action parameter receives the whole object and runs only
for that specific action. This makes it the right choice for two distinct
situations: rules that only apply in a particular context (a seat count check
that matters for registerAction() but not for showAction()), and
rules that span multiple properties. For example: if a conference starts more
than four weeks from now, a speaker assignment is optional — but if it starts
sooner, a speaker is required. That rule cannot be expressed on any single
property; it needs both $startDate and $speaker at the same time:
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
class ConferenceSpeakerValidator extends AbstractValidator
{
protected function isValid(mixed $value): void
{
$weeksUntilStart = (int)(($value->getStartDate()->getTimestamp() - time()) / 604800);
if ($weeksUntilStart < 4 && $value->getSpeaker() === null) {
$this->addErrorForProperty(
'speaker',
$this->translateErrorMessage('my_extension.messages:validator.conference.speakerRequired'),
1716300100,
);
}
}
}
Both approaches can be combined on the same type — property validators run first, and action-parameter validators run afterwards.
On this page
Structure of a custom validator
Custom validators extend
\TYPO3\ and
implement a single method
is. If a value
fails validation, call
$this->add or
$this->add — do not throw an exception and do not
return anything. The framework reads
$this->result after
is returns.
Place validators in Classes/ inside your
extension, following the naming convention
<Subject>Validator:
<?php
namespace MyVendor\MyExtension\Validation\Validator;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
class ConferenceDateValidator extends AbstractValidator
{
protected string $message = 'my_extension.messages:validator.conference.endBeforeStart';
protected array $translationOptions = ['message'];
protected $supportedOptions = [
'message' => [null, 'Custom translation key or message text', 'string'],
];
protected function isValid(mixed $value): void
{
if ($value->getEndDate() <= $value->getStartDate()) {
$this->addErrorForProperty(
'endDate',
$this->translateErrorMessage($this->message),
1716299001,
);
}
}
}
Key points:
- Do not declare the class as
final— the same convention that applies to controllers applies here. Third parties can extend the validator. $accepts(the default) meansEmpty Values = true isis skipped when a value isValid () nullor an empty string. Set it tofalseonly if a validator should explicitly reject empty values (asNotdoes).Empty Validator - The error code is an arbitrary integer that must be unique across your extension. Using the Unix timestamp at the time of writing is a convenient way to generate a unique number.
- Translation keys use the domain syntax introduced in TYPO3 v14:
my_extension.messages:some.keyresolves toEXT:my_extension/Resources/Private/Language/locallang.xlf. See Translation domain syntax as shorter alternative to LLL:EXT: (TYPO3 v14) for the full syntax including non-default language files.
Reporting errors on a specific property
$this->add attaches the error to a named property of
the validated object rather than to the object itself. The
Form.validationResults ViewHelper <f:form.validationResults> view helper can then
display the message adjacent to the right form field:
<f:form.validationResults for="conference.endDate">
<f:for each="{validationResults.errors}" as="error">
<p class="error">{error.message}</p>
</f:for>
</f:form.validationResults>
Use
$this->add instead when the error applies to the whole
object and does not belong to any single field.
Supporting options and substitution values in messages
If your validator needs to be configured, for example, there needs to be a
minimum seat count, declare the options in
$supported. Each option contains
an array with values [default, description, type]. Access resolved values using
$this->options:
<?php
namespace MyVendor\MyExtension\Validation\Validator;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;
class SeatCountValidator extends AbstractValidator
{
protected string $message = 'my_extension.messages:validator.conference.notEnoughSeats';
protected array $translationOptions = ['message'];
protected $supportedOptions = [
'minimum' => [1, 'Minimum number of available seats required', 'integer'],
'message' => [null, 'Custom translation key or message text', 'string'],
];
protected function isValid(mixed $value): void
{
$minimum = $this->options['minimum'];
if ($value->getAvailableSeats() < $minimum) {
$this->addError(
$this->translateErrorMessage($this->message, '', [$minimum]),
1716300001,
[$minimum],
);
}
}
}
The error message in locallang. uses a %s placeholder:
<trans-unit id="validator.conference.notEnoughSeats">
<source>At least %s seat(s) must be available for registration.</source>
</trans-unit>
translate accepts the values to be substituted as its third
argument and passes them through
vsprintf. The same values are passed
as the third argument to
add. They are stored in the
Error object for programmatic
access:
$this->addError(
$this->translateErrorMessage($this->message, '', [$minimum]),
1716300001, // An arbitrary unique number, for example the timestamp when writing the code
[$minimum],
);
Attach the validator with its option on the action parameter:
use MyVendor\MyExtension\Validation\Validator\SeatCountValidator;
use TYPO3\CMS\Extbase\Attribute\Validate;
public function registerAction(
#[Validate(SeatCountValidator::class, options: ['minimum' => 1])]
Conference $conference,
): ResponseInterface {
// Only reached when SeatCountValidator passes
}
Making the error message overridable
To allow callers to override the message text via the options array
without having to subclass (the same mechanism used by built-in validators) declare
a
protected string $message property, add it to
$translation, and list it in
$supported. The
Seat above follows this pattern. A caller can
then replace the message at the call site:
#[Validate(SeatCountValidator::class, options: [
'minimum' => 1,
'message' => 'my_extension.messages:validator.conference.notEnoughSeats.short',
])]
What to read next
- Built-in validators and the #[Validate] attribute — check whether a built-in validator already covers a constraint before writing a custom one.
- Validation in Extbase — how the framework triggers
errorand how to display errors in Fluid templates.Action ()