Symfony Validator en PHP: validar objetos con constraints y grupos de validación

El componente Validator de Symfony permite validar objetos PHP mediante constraints declaradas como atributos. Funciona de forma independiente a Symfony Framework y es una de las librerías de validación más completas del ecosistema PHP.

Instalación

composer require symfony/validator

Configurar el ValidatorBuilder

<?php
use SymfonyComponentValidatorValidation;

$validator = Validation::createValidatorBuilder()
    ->enableAttributeMapping()  // Usar atributos PHP #[...]
    ->getValidator();
?>

Constraints como atributos

<?php
use SymfonyComponentValidatorConstraints as Assert;

class RegistroUsuario
{
    public function __construct(
        #[AssertNotBlank(message: 'El nombre es obligatorio')]
        #[AssertLength(min: 2, max: 100)]
        public readonly string $nombre,

        #[AssertNotBlank]
        #[AssertEmail(message: 'El email "{{ value }}" no es válido')]
        public readonly string $email,

        #[AssertNotBlank]
        #[AssertLength(min: 8, message: 'La contraseña debe tener al menos {{ limit }} caracteres')]
        #[AssertRegex(pattern: '/[A-Z]/', message: 'Debe contener al menos una mayúscula')]
        public readonly string $password,

        #[AssertRange(min: 18, max: 120)]
        public readonly int $edad,
    ) {}
}
?>

Validar y obtener errores

<?php
$datos = new RegistroUsuario(
    nombre:   'A',
    email:    'no-es-un-email',
    password: 'corta',
    edad:     15,
);

$errores = $validator->validate($datos);

if (count($errores) > 0) {
    foreach ($errores as $error) {
        echo $error->getPropertyPath() . ': ' . $error->getMessage() . "n";
    }
}
// nombre: Esta cadena es demasiado corta. Debe tener 2 caracteres o más.
// email: El email "no-es-un-email" no es válido
// password: La contraseña debe tener al menos 8 caracteres
// password: Debe contener al menos una mayúscula
// edad: Esta value debe ser entre 18 y 120
?>

Grupos de validación

Los grupos permiten aplicar distintas reglas según el contexto (crear vs. actualizar):

<?php
class Producto
{
    #[AssertNotBlank(groups: ['crear', 'actualizar'])]
    #[AssertLength(min: 3, groups: ['crear', 'actualizar'])]
    public string $nombre = '';

    #[AssertNotBlank(groups: ['crear'])]          // Solo obligatorio al crear
    #[AssertPositive(groups: ['crear', 'actualizar'])]
    public ?float $precio = null;

    #[AssertNotBlank(groups: ['crear'])]
    #[AssertFile(mimeTypes: ['image/jpeg', 'image/png'], groups: ['crear'])]
    public mixed $imagen = null;
}

// Validar solo para creación
$erroresCrear    = $validator->validate($producto, null, ['crear']);

// Validar solo para actualización
$erroresActualizar = $validator->validate($producto, null, ['actualizar']);
?>

Colecciones con #[All]

<?php
class PedidoInput
{
    #[AssertNotBlank]
    #[AssertAll([
        new AssertType('integer'),
        new AssertPositive(),
    ])]
    public array $idProductos = [];

    #[AssertCount(min: 1, max: 100, minMessage: 'El pedido debe tener al menos un producto')]
    public array $cantidades = [];
}
?>

Constraints personalizadas

<?php
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintValidator;

#[Attribute]
class SlugUnico extends Constraint
{
    public string $message = 'El slug "{{ value }}" ya está en uso';
}

class SlugUnicoValidator extends ConstraintValidator
{
    public function __construct(private readonly PDO $pdo) {}

    public function validate(mixed $value, Constraint $constraint): void
    {
        if (null === $value || '' === $value) {
            return; // Deja que NotBlank se encargue de los vacíos
        }

        $stmt = $this->pdo->prepare('SELECT 1 FROM articulos WHERE slug = ?');
        $stmt->execute([$value]);

        if ($stmt->fetchColumn()) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ value }}', $value)
                ->addViolation();
        }
    }
}
?>

Constraints útiles adicionales

ConstraintUso
#[AssertUrl]Valida URLs
#[AssertIsbn]ISBN-10 e ISBN-13
#[AssertUuid]Formato UUID
#[AssertDateTime]Fecha con formato específico
#[AssertChoice]Valor dentro de una lista
#[AssertUnique]Sin duplicados en un array
#[AssertCardScheme]Número de tarjeta de crédito
#[AssertIban]IBAN bancario

Errores comunes

  • Atributos no detectados: asegúrate de llamar a enableAttributeMapping() en el ValidatorBuilder; sin ello, los atributos #[Assert...] se ignoran.
  • Grupos vacíos devuelven sin errores: si validas con un grupo que no existe, validate() devuelve una lista vacía. Los nombres de grupo distinguen mayúsculas.
  • Validator personalizado sin inyección: si el validator necesita servicios (como PDO), debes registrarlo en el contenedor o pasarlo manualmente al builder.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP