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
| Constraint | Uso |
|---|---|
#[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.
