symfony/validator
Symfony Validator provides a flexible validation system based on the JSR-303/Bean Validation model. Define constraints via annotations/attributes, YAML/XML, or PHP, validate objects and values, and get detailed, localized violation messages.
Installation:
composer require symfony/validator
Laravel already includes this package via Symfony components, so no additional configuration is needed.
First Use Case: Validate a DTO or request payload in a Laravel controller:
use Symfony\Component\Validator\Validator\ValidatorInterface;
public function store(Request $request, ValidatorInterface $validator)
{
$data = $request->validate([
'email' => 'required|email',
'age' => 'integer|min:18',
]);
$errors = $validator->validate($data);
if (count($errors) > 0) {
return response()->json(['errors' => (string) $errors], 400);
}
// Proceed with valid data
}
Key Classes to Know:
ValidatorInterface: Core validator service (injected via Laravel's container).Constraint: Base class for custom validation rules.ConstraintValidator: Validates a specific constraint.ConstraintViolationListInterface: Holds validation results.Where to Look First:
Request::validate() (uses Symfony Validator under the hood).app/Providers/AppServiceProvider.php for global validation configurations.Leverage Laravel’s FormRequest to auto-validate with Symfony’s constraints:
use Symfony\Component\Validator\Constraints as Assert;
class StoreUserRequest extends FormRequest
{
public function rules()
{
return [
'name' => 'required|string|max:255',
'email' => ['required', 'email', new Assert\Email()],
];
}
public function authorize()
{
return true;
}
public function validated()
{
$validator = app(ValidatorInterface::class);
$errors = $validator->validate($this->all());
if ($errors->count() > 0) {
throw new \Illuminate\Validation\ValidationException($validator->validate($this->all()));
}
return parent::validated();
}
}
Create reusable validation rules:
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
#[Attribute]
class ValidPassword extends Constraint
{
public string $message = 'The password must contain at least one uppercase letter, one lowercase letter, and one number.';
}
class ValidPasswordValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', $value)) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Usage:
use App\Validator\Constraints\ValidPassword;
#[ValidPassword]
public string $password;
Validate different subsets of data:
$validator = app(ValidatorInterface::class);
$violations = $validator->validate($user, null, ['registration']); // Only validate 'registration' group
Reuse validation logic across services:
class UserService
{
public function __construct(private ValidatorInterface $validator) {}
public function createUser(array $data): void
{
$violations = $this->validator->validate($data);
if ($violations->count() > 0) {
throw new \RuntimeException((string) $violations);
}
// Save user
}
}
Format validation errors for JSON APIs:
$errors = $validator->validate($data);
return response()->json([
'errors' => collect($errors)->map(fn ($error) => $error->getPropertyPath() . ': ' . $error->getMessage())
]);
Extend Symfony’s validator in AppServiceProvider:
public function register()
{
$this->app->extend(ValidatorInterface::class, function ($validator) {
$validator->setMappingCacheDir(storage_path('framework/cache/validator'));
return $validator;
});
}
Circular References in Validation:
User->posts->author->user). Use @Assert\Valid sparingly or disable it for specific properties.Performance with Large Datasets:
cache_dir in config). For dynamic validation rules, disable caching or use ValidatorBuilder to rebuild the validator:
$validator = ValidatorBuilder::create()
->enableAnnotationMapping()
->getValidator();
Constraint Overrides:
#[Assert\Length(min: 5)]) may not work as expected if the parent constraint’s options aren’t properly inherited. Use #[Assert\Callback] for complex logic:
#[Assert\Callback]
public function validate($value, ExecutionContextInterface $context)
{
if (!str_contains($value, 'laravel')) {
$context->buildViolation('Must contain "laravel".')
->addViolation();
}
}
Validation Groups in Laravel:
FormRequest doesn’t natively support Symfony’s validation groups. Use a custom validator or manually pass groups:
$validator = app(ValidatorInterface::class);
$validator->validate($data, null, ['group1', 'group2']);
Expression Language Quirks:
Expression constraint (@Assert\Expression) requires the symfony/expression-language component. Install it separately:
composer require symfony/expression-language
#[Assert\Expression(
expression: "this.getAge() >= 18",
message: "You must be at least 18 years old."
)]
Type Safety:
#[Assert\Type(type: 'string')] or #[Assert\Type(type: 'array')].Inspect Violations:
$violations = $validator->validate($data);
foreach ($violations as $violation) {
dump([
'property' => $violation->getPropertyPath(),
'message' => $violation->getMessage(),
'code' => $violation->getCode(),
]);
}
Enable Validation Tracing:
framework.validation.traceable to true in Laravel’s config/services.php to log validation calls.Constraint Dumping:
php artisan vendor:publish --tag=validator to publish Symfony’s constraint templates (e.g., YAML/XML mappings).Common Error Codes:
SYMFONY\Component\Validator\Constraints\Length: Use min/max in error messages:
#[Assert\Length(
min: 5,
minMessage: 'This value should be at least {{ limit }} characters long.',
max: 20,
maxMessage: 'This value should be at most {{ limit }} characters long.'
)]
Custom Constraint Validators:
ConstraintValidatorInterface for complex rules:
class CustomValidator extends AbstractConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if ($value !== $constraint->expected) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $value)
->addViolation();
}
}
}
Validation Event Listeners:
$dispatcher = app(\Symfony\Component\EventDispatcher\EventDispatcherInterface::class);
$dispatcher->addListener(ConstraintViolationListEvent::class, function ($event) {
// Log violations or modify them
});
Override Default Constraints:
Email) by binding a custom validator in Laravel’s service container:
$this->app->bind(EmailValidator::class, function () {
return new CustomEmailValidator();
});
Validation in Tests:
ValidatorInterface in PHPUnit tests:
public function testValidation()
{
$validator = $this->app->make(ValidatorInterface::class);
$violations = $validator->validate(['email' => 'invalid']);
$this->assertCount(1, $violations);
}
Laravel Policy Integration:
How can I help you explore Laravel packages today?