Validating Doctrine entities with only the @ORM\Column annotation

Validating Doctrine entities with Symfony validator is a very common use case in a web application. It gets its own page in the Symfony documentation and is done like this:

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
Class User
{
/**
* @Assert\Email()
* @Assert\Length(max=255)
* @Assert\NotNull()
* @ORM\Column(type=”string”, length=255)
*/
protected $email;

}

At AssoConnect we have more than 100 different entities so we have a lot of this kind of code.
This kind of declaration is way too verbose:
- We repeat two times the 255 chars limit for each email property
- We have both constraints for each email property
- The column is obviously a string type as it contains an email
- The NotNull constraint is a duplicate of the Doctrine nullable property (false is the default value)

And email is not the only use case: think about column holding IBAN, money/credit, latitude, longitude, UUID, phone numbers, datetime, …

So here comes our Doctrine Validator bundle. It uses custom Doctrine DBAL types along with our Validator bundle to validate Doctrine entities with only the configuration of the column. Let’s see how it works.

First, some opiniated thoughts about Symfony constraints. Almost all of them are very useful, perform validation with complex rules and can be used as is.

But some of them are not adapted to real world use cases (like hello@foo.bar passing EmailValidator checks with loose, html5 and strict modes) or missing like a PhoneValidator (we are considering submitting a PR). We have “fixed” this with our Validator bundle.

Then comes the main part of Doctrine Validator bundle. Doctrine provides very detailed metadata for each entity field that we can use to check for string length, float scale precision, nullable or not nullable.

For instance, let’s say we want to store the email address of a user in an emailAddress entity property with a 100 characters length limit: the annotation is @ORM\Column(type="string", length=100).

Then the following line of code will give us:

$fieldMapping = $this->em->getClassMetadata(User::class)->fieldMappings[‘emailAddress’]
  • $fieldMapping['nullable']: if false, then we have to use NotNull()constraint
  • $fieldMapping['length']: 100 which means Length($fieldMapping['length'])constraint
    Note: varchar length unit if character whereas text length unit is bit. So 💩will count for 1 in a varchar column and for 4 in a text column. Our bundle supports this difference.

We are missing the Email() constraint so we have created many custom Doctrine types for common use cases and a switch loop on $fieldMapping['type'] returns the right constraint to evaluate.
Our User entity class is now much more concise and there is no more forgotten constraints!

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use AssoConnect\DoctrineValidatorBundle\Validator\Constraints as AssoConnectAssert;
/**
* @AssoConnectAssert\Entity()
*/
Class User
{
/**
* @ORM\Column(type=”email”, length=100)
*/
protected $email;

}

Happy coding!