Symfony custom validation constraint
12 May 2025 (Updated 18 May 2025)
Recipe
1. Create the constraint class
<?php
// src/Validator/ContainsAlphanumeric.php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ContainsAlphanumeric extends Constraint
{
public string $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
public string $mode = 'strict';
// all configurable options must be passed to the constructor
public function __construct(?string $mode = null, ?string $message = null, ?array $groups = null, $payload = null)
{
parent::__construct([], $groups, $payload);
$this->mode = $mode ?? $this->mode;
$this->message = $message ?? $this->message;
}
}
2. Create the validator
In the base Symfony\Component\Validator\Constraint
class, you’ll see the following code which tells Symfony where to find the constraint’s validator class:
// in the base Symfony\Component\Validator\Constraint class
public function validatedBy(): string
{
return static::class.'Validator';
}
This means that by default, if you create a ContainsAlphanumeric
constraint class, then Symfony will look for a ConstainsAlphanumericValidator
class for the actual validator.
Let’s stick with the defaults and create that class:
<?php
// src/Validator/ContainsAlphanumericValidator.php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ContainsAlphanumericValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof ContainsAlphanumeric) {
throw new UnexpectedTypeException($constraint, ContainsAlphanumeric::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) to take care of that
if (null === $value || '' === $value) {
return;
}
if (!is_string($value)) {
// throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
throw new UnexpectedValueException($value, 'string');
// separate multiple types using pipes
// throw new UnexpectedValueException($value, 'string|int');
}
// access your configuration options like this:
if ('strict' === $constraint->mode) {
// ...
}
if (preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
return;
}
// the argument must be a string or an object implementing __toString()
$this->context->buildViolation($constraint->message)
->setParameter('{{ string }}', $value)
->addViolation();
}
}
3. Use the validator
<?php
// src/Entity/AcmeEntity.php
namespace App\Entity;
use App\Validator as AcmeAssert;
use Symfony\Component\Validator\Constraints as Assert;
class AcmeEntity
{
// ...
#[Assert\NotBlank]
#[AcmeAssert\ContainsAlphanumeric(mode: 'loose')]
protected string $name;
// ...
}
Links
Tagged:
Symfony recipes