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 request payload in a Laravel controller:
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Constraints as Assert;
public function store(Request $request, ValidatorInterface $validator)
{
$data = $request->validate([
'email' => ['required', 'email'],
'age' => ['integer', 'min' => 18, 'max' => 120],
]);
// Manual validation (alternative to Laravel's built-in)
$errors = $validator->validate($data, [
new Assert\Email(),
new Assert\GreaterThanOrEqual(18),
new Assert\LessThanOrEqual(120),
]);
if (count($errors) > 0) {
return response()->json(['errors' => (string) $errors], 400);
}
// Proceed with logic...
}
Where to Look First:
Request::validate() (which uses Symfony Validator under the hood)ValidatorInterface for programmatic validation.Extend Laravel’s FormRequest and leverage Symfony’s constraints:
use Illuminate\Foundation\Http\FormRequest;
use Symfony\Component\Validator\Constraints as Assert;
class StoreUserRequest extends FormRequest
{
public function rules()
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
];
}
public function withValidator($validator)
{
$validator->addConstraintTo('email', new Assert\NotBlank());
$validator->addConstraintTo('age', new Assert\Type('integer'));
}
}
Create reusable validation rules:
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class UniqueUsername extends Constraint
{
public $message = 'This username is already taken.';
}
class UniqueUsernameValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (User::where('username', $value)->exists()) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Usage:
$validator->validate($data, [
new UniqueUsername(),
]);
Validate subsets of data:
use Symfony\Component\Validator\Constraints\GroupSequence;
$validator->validate($data, new GroupSequence([
'Default',
'createUser',
]));
Define groups in your DTO:
use Symfony\Component\Validator\Constraints as Assert;
class UserDto
{
#[Assert\NotBlank(groups: ['createUser'])]
public string $password;
#[Assert\Email(groups: ['Default'])]
public string $email;
}
Inject ValidatorInterface into services:
class UserService
{
public function __construct(private ValidatorInterface $validator) {}
public function create(array $data): void
{
$errors = $this->validator->validate($data, [
new Assert\All([
new Assert\Type('string'),
new Assert\NotBlank(),
]),
]);
if ($errors->count() > 0) {
throw new \InvalidArgumentException((string) $errors);
}
}
}
Format validation errors for JSON APIs:
public function validateAndRespond(Request $request, ValidatorInterface $validator)
{
$data = $request->all();
$errors = $validator->validate($data, [
new Assert\Collection([
'email' => [new Assert\Email()],
'age' => [new Assert\Range(['min' => 18])],
]),
]);
if ($errors->count() > 0) {
return response()->json([
'errors' => array_map(function ($error) {
return $error->getPropertyPath() . ': ' . $error->getMessage();
}, iterator_to_array($errors)),
], 422);
}
}
Use closures for runtime validation:
use Symfony\Component\Validator\Expression;
$validator->validate($data, [
new Assert\Expression(
'value > 10',
['value' => $data['value']],
'Value must be greater than 10.'
),
]);
Validator facade (wraps Symfony’s ValidatorInterface):
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($data, [
'email' => 'required|email',
]);
use Illuminate\Contracts\Validation\Rule;
class UniqueUsernameRule implements Rule
{
public function passes($attribute, $value)
{
return !User::where('username', $value)->exists();
}
public function message()
{
return 'The :attribute is already taken.';
}
}
Usage in Laravel:
$validator->addRules([
'username' => ['required', new UniqueUsernameRule],
]);
Constraint Order Matters:
NotBlank) before broader ones (e.g., Type).Circular References:
User hasMany Posts, Post belongsTo User) can cause infinite loops. Use ValidationContext::disableOriginalValueNormalization() or lazy-loading.Lazy-Loading Properties:
ValidationContext::disableOriginalValueNormalization() or load relations beforehand:
$user->load('posts');
$validator->validate($user);
Nested Arrays/Objects:
Assert\Collection or Assert\All:
new Assert\Collection([
'field' => new Assert\All([
new Assert\Type('string'),
]),
]);
Custom Constraint Caching:
$validator->getMetadataFactory()->clearCache();
Deprecations in Symfony 8+:
GroupSequence no longer accepts associative arrays (use indexed arrays or GroupSequenceProvider).Performance with Large Datasets:
ValidationContext::disableOriginalValueNormalization() or validate subsets.Inspect Violations:
$errors = $validator->validate($data);
foreach ($errors as $error) {
dump([
'property' => $error->getPropertyPath(),
'message' => $error->getMessage(),
'code' => $error->getCode(),
]);
}
Enable Validation Groups:
groups to isolate validation logic:
new Assert\NotBlank(groups: ['registration']),
Constraint Violation Codes:
$errors->findByCodes(['unique']);
Disable Normalization:
$validator->validate($data, [], null, [
'disableOriginalValueNormalization' => true,
]);
Custom Constraint Validators:
ConstraintValidator for complex logic:
class CustomValidator extends ConstraintValidator
{
protected function validateCustom($value, Constraint $constraint)
{
if (!preg_match($constraint->pattern, $value)) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Validation Context:
$context = $this->context;
$root = $context->getRoot();
Constraint Compilation:
$metadataFactory = new MetadataFactory();
$metadataFactory->setMetadataCache(new FileCache(sys_get_temp_dir()));
$validator = new Validator($metadataFactory);
Laravel Service Provider:
How can I help you explore Laravel packages today?