Validator

Custom validators are located in the directory Classes/Domain/Validator and therefore in the namespace Vendor\MyExtension\Domain\Validator.

All validators extend the AbstractValidator (\TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator).

Custom validator for a property of the domain model

When the standard validators provided by Extbase are not sufficient you can write a custom validators to use on the property of a domain model:

Class T3docs\BlogExample\Domain\Validator\TitleValidator
final class TitleValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        // $value is the title string
        if (str_starts_with('_', $value)) {
            $errorString = 'The title may not start with an underscore. ';
            $this->addError($errorString, 1297418976);
        }
    }
}
Copied!

The method isValid() does not return a value. In case of an error it adds an error to the validation result by calling method addError(). The long number added as second parameter of this function is the current UNIX time in the moment the error message was first introduced. This way all errors can be uniquely identified.

This validator can be used for any string property of model now by including it in the annotation of that parameter:

EXT:blog_example/Classes/Domain/Model/Blog.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Domain\Model;

use T3docs\BlogExample\Domain\Validator\TitleValidator;
use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class Blog extends AbstractEntity
{
    #[Validate([
        'validator' => TitleValidator::class,
    ])]
    public string $title = '';
    /**
     * Use annotations instead for compatibility with TYPO3 v11 and PHP 7.4:
     * @Validate("T3docs\BlogExample\Domain\Validator\TitleValidator")
     */
    public string $title2 = '';
}
Copied!

Complete domain model validation

At certain times in the life cycle of a model it can be necessary to validate the complete domain model. This is usually done before calling a certain action that will persist the object.

Class T3docs\BlogExample\Domain\Validator\BlogValidator
use T3docs\BlogExample\Domain\Model\Blog;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

final class BlogValidator extends AbstractValidator
{
    protected function isValid(mixed $value): void
    {
        if (!$value instanceof Blog) {
            $errorString = 'The blog validator can only handle classes '
                . 'of type T3docs\BlogExample\Domain\Validator\Blog. '
                . $value::class . ' given instead.';
            $this->addError($errorString, 1297418975);
        }
        if (!$this->blogValidationService->isBlogCategoryCountValid($value)) {
            $errorString = LocalizationUtility::translate(
                'error.Blog.tooManyCategories',
                'BlogExample'
            );
            // Add the error to the property if it is specific to one property
            $this->addErrorForProperty('categories', $errorString, 1297418976);
        }
        if (!$this->blogValidationService->isBlogSubtitleValid($value)) {
            $errorString = LocalizationUtility::translate(
                'error.Blog.invalidSubTitle',
                'BlogExample'
            );
            // Add the error directly if it takes several properties into account
            $this->addError($errorString, 1297418974);
        }
    }
}
Copied!

If the error is related to a specific property of the domain object, the function addErrorForProperty() should be used instead of addError().

The validator is used as annotation in the action methods of the controller:

EXT:blog_example/Classes/Controller/BlogController.php, modified
<?php

declare(strict_types=1);

namespace T3docs\BlogExample\Controller;

use Psr\Http\Message\ResponseInterface;
use T3docs\BlogExample\Domain\Model\Blog;
use T3docs\BlogExample\Exception\NoBlogAdminAccessException;
use TYPO3\CMS\Extbase\Annotation\Validate;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class BlogController extends ActionController
{
    /**
     * Updates an existing blog
     *
     * $blog is a not yet persisted clone of the original blog containing
     * the modifications
     *
     * @Validate(param="blog", validator="FriendsOfTYPO3\BlogExample\Domain\Validator\BlogValidator")
     * @throws NoBlogAdminAccessException
     */
    public function updateAction(Blog $blog): ResponseInterface
    {
        // do something
        return $this->htmlResponse();
    }
}
Copied!

Dependency injection in validators

Starting with TYPO3 v12 Extbase validators are capable of dependency injection without further configuration, you can use the constructor method:

EXT:my_extension/Classes/Validators/MyCustomValidator.php
<?php

namespace MyVendor\MyExtension\Validators;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

final class MyCustomValidator extends AbstractValidator
{
    public function __construct(private readonly MyService $myService) {}

    protected function isValid(mixed $value): void
    {
        // TODO: Implement isValid() method.
    }
}
Copied!

Extensions that want to support both TYPO3 v12 and v11 have to implement the method setOptions and use the injector method for dependency injection:

EXT:my_extension/Classes/Validators/MyCustomValidator.php
<?php

namespace MyVendor\MyExtension\Validators;

use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

final class MyCustomValidator extends AbstractValidator
{
    private MyService $myService;

    public function injectMyService(MyService $myService)
    {
        $this->myService = $myService;
    }

    public function setOptions(array $options): void
    {
        // This method is upwards compatible with TYPO3 v12, it will be implemented
        // by AbstractValidator in v12 directly and is part of v12 ValidatorInterface.
        // @todo: Remove this method when v11 compatibility is dropped.
        $this->initializeDefaultOptions($options);
    }

    protected function isValid(mixed $value): void
    {
        // TODO: Implement isValid() method.
    }
}
Copied!

Additionally, the validator requiring dependency injection has to be registered in the extension's Services.yaml until TYPO3 v11 support is dropped:

EXT:my_extension/Configuration/Services.yaml
services:
  # This is obsolete when the extension does not support TYPO3 v11 anymore.
  # @todo: Remove this when v11 compatibility is dropped.
  MyVendor\MyExtension\Validators\MyCustomValidator:
    public: true
    shared: false
Copied!