Extending of a Symfony constraint in order to add your own validation rules

In this post will see how to extend a native Symfony constraint in order to add some custom validation rules. We will see a concrete example with the Email constraint on which we will add a new rule that prevents an email without domain extension to pass the validation.

Posted on 2019-02-14 by (Updated on 2019-02-21)

Symfony  Symfony4  validation  email  form 

Published in "A week of Symfony n°633" on Symfony.com

Extending of a Symfony constraint in order to add your own validation rules

Goal

The goal will be to make adresses like "myemail@yahoo" invalid as the domain extension is missing. Those types of email are indeed valid from the RFC point of vue but concretely all services just won't work with thoses addresses. Note that using the "loose" mode of the native constraint can lead to the same result but in this case you lose the benefit of using the strict mode. (for example the email "toto@toto@toto.toto" is considered valid when using the "loose" mode)

Configuration

This post was written using the following components:

Pre-requisites

We will assume that you are already familiar with Symfony, the Form component and you know how to create a basic form with a field with some validation constraints. If that's not case, check out the documentation right now! 🤓

Let's go!

When wanting to create a form constraint one needs two classes: The constraint and the validator.

The constraint

This file is used to declare the configuration. Let's look at the code: (the file you are viewing is in fact the real file used in this project thanks to a view source code Twig helper)

<?php

declare(strict_types=1);

namespace App\Component\Validator;

use Symfony\Component\Validator\Constraints\Email as BaseEmail;

/**
 * New email validation rule, "forbid values without domain extension".
 */
class Email extends BaseEmail
{
    final public const INVALID_DOMAIN_EXTENSION = '7da53a8b-56f3-4288-bb3e-ee9ede4ef9a2';
    public bool $forbidEmptyDomainExtension = false;
    public string $messageInvalidDomainExtension = 'bad_email_domain_extension';
}

As you can see we extend the native Email constraint and we have three configuration items.

The validator

<?php

declare(strict_types=1);

namespace App\Component\Validator;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\EmailValidator as BaseEmailValidator;

/**
 * Provide an additional check to avoid empty domain extensions.
 */
class EmailValidator extends BaseEmailValidator
{
    public function validate($value, Constraint $constraint): void
    {
        parent::validate($value, $constraint);

        if ($value === null || $value === '') {
            return;
        }

        if (!$constraint instanceof Email) {
            throw new \RuntimeException('Unexpected constraint type bound to the validator.');
        }

        if (!$constraint->forbidEmptyDomainExtension) {
            return;
        }

        $domain = explode('@', (string) $value)[1] ?? '';
        $extension = explode('.', $domain)[1] ?? false;
        if (!$extension) {
            $this->context->buildViolation($constraint->messageInvalidDomainExtension)
                ->setCode(Email::INVALID_DOMAIN_EXTENSION)
                ->addViolation();
        }
    }
}

Again here, we extend the native validator. In the main validate() function we call the parent one and continue only if we have a value to handle.
Next, we check the type of the constraint to have the autocompletion of the class Email constants and attributes.
Then, we check the activation of the new validation rule. The main validation code will check that the value has a domain extension. And eventually, if the extension is not found we add a violation to the context associated with the validator. It will be used to display the error on the form.

Usage

To use our new constraint, replace the use statement in your form type class like below:

<?php

declare(strict_types=1);

namespace App\Form\Type\User;

Then in the buildForm function of your form type, use the new constraint and activate its custom validation rule: (note that all other options of the native Email constraint are still available)

public function buildForm(FormBuilderInterface $b, array $options): void
{
    $b->add('email', EmailType::class, [
            'label' => 'form.email',
            'constraints' => [
                new NotBlank(),
                new Email(['mode' => 'strict', 'forbidEmptyDomainExtension' => true]),
            ]
    ]);

And finally, don't forget to add the translation of the error message to your i18n file:

# translations/validators.en.yml
bad_email_domain_extension: >
    You have forgotten the extension of your email's domain, og: ".com"

That's it! 😁

Conclusion

Of course this was a basic example but you can add more complex validation rules. Your can directly test this constraint on the registration form of this website.

PS: Note that the files' paths will be different (remove the bundle prefix) if you are using Flex: Tokeeen doesn't use it yet even it is powered by Symfony 4.2.

Call to action

Did you like these posts? You can help us back in several ways:


Thank you for reading! And see you soon on Tokeeen! 😉

COil


profile for Tokeeen.com at Stack Overflow, Q&A for professional and enthusiast programmers