choz/request-validation-bundle
Installation:
Run composer require choz/request-validation-bundle in your Symfony project.
Enable the bundle in config/bundles.php:
Choz\RequestValidationBundle\ChozRequestValidationBundle::class => ['all' => true],
First Use Case:
Create a request validation class (e.g., TagCreateRequest) extending BaseRequest and define validation rules in the rules() method:
use Choz\RequestValidationBundle\Request\BaseRequest;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\Required;
use Symfony\Component\Validator\Constraints\Type;
class TagCreateRequest extends BaseRequest {
protected function rules(): array {
return [
new Collection([
'id' => [new Required(), new Type('int')],
'name' => [new Required(), new Type('string')],
]),
];
}
}
Inject and Use: Inject the request class into your controller and access validated data via getter methods:
#[Route('/tags', methods: ['POST'])]
public function create(TagCreateRequest $request): JsonResponse {
$id = $request->getInteger('id');
$name = $request->getString('name');
// Use validated data
}
Error Handling: The bundle automatically returns a structured JSON error response (HTTP 400) for invalid requests:
{
"message": "The given data failed to pass validation.",
"errors": {
"id": ["This field is missing."],
"name": ["This value should be of type string."]
}
}
BaseRequest subclass, keeping controllers clean.UserUpdateRequest for both PUT /users/{id} and PATCH /users/{id}).getInteger(), getString()) to enforce type safety at the request level:
public function getId(): int {
return $this->getInteger('id');
}
@Assert\Callback) for complex rules:
use Symfony\Component\Validator\Constraints\Callback;
protected function rules(): array {
return [
new Collection([
'email' => [
new Required(),
new Type('string'),
new Callback([$this, 'validateCustomEmail']),
],
]),
];
}
public function validateCustomEmail($value, Constraint $constraint) {
if (!str_contains($value, 'example.com')) {
return 'Email must be from example.com';
}
}
symfony-bundles/json-request-bundle for seamless JSON payload validation.Collection constraints for nested objects/arrays:
new Collection([
'user' => [
new Collection([
'name' => [new Required(), new Type('string')],
'roles' => [new Type('array')],
]),
],
]);
BaseRequest and assert validation errors:
public function testInvalidRequest() {
$request = new TagCreateRequest();
$request->setData(['id' => 'invalid', 'name' => 123]);
$this->expectException(ValidationFailedException::class);
$request->validate();
}
HttpClient to send malformed requests and verify error responses:
$response = $client->request('POST', '/tags', [
'json' => ['id' => 'not_an_int'],
]);
$this->assertEquals(400, $response->getStatusCode());
new GroupSequence(['create', 'update'])).Missing JsonRequestBundle for JSON APIs:
symfony-bundles/json-request-bundle, JSON payloads may not be parsed correctly.json_request is enabled in config/packages/framework.yaml:
framework:
json_request:
enabled: true
Overriding Default Error Responses:
ValidationFailedException by default. Customizing error formats requires overriding the event listener (see below).response_code in config/packages/choz_request_validation.yaml to change the HTTP status (e.g., 422 for HTTP_UNPROCESSABLE_ENTITY):
choz_request_validation:
response_code: !php/const Symfony\Component\HttpFoundation\Response::HTTP_UNPROCESSABLE_ENTITY
Type Safety in Getters:
getInteger() throw exceptions if the field is missing or invalid. Avoid silent failures by wrapping calls:
try {
$id = $request->getInteger('id');
} catch (ValidationFailedException $e) {
// Handle missing/invalid field
}
Constraint Order Matters:
Required before Type to fail fast:
new Collection([
'id' => [new Required(), new Type('int')], // Correct
// vs.
'id' => [new Type('int'), new Required()], // May validate type on missing field
]);
Circular Dependencies:
Collection constraints (e.g., user.address.city referencing user). Use Callback constraints for dynamic validation.Enable Symfony Debug Mode:
Log Validation Errors:
// src/EventListener/CustomRequestValidationEventListener.php
public function onKernelException(GetResponseForExceptionEvent $event) {
$exception = $event->getThrowable();
if ($exception instanceof ValidationFailedException) {
error_log('Validation errors: ' . print_r($exception->getErrors(), true));
}
}
Validate Raw Data:
ValidatorInterface directly to debug constraints:
$validator = $this->container->get('validator');
$errors = $validator->validate($data, $constraints);
Custom Error Formatters:
# config/services.yaml
services:
App\EventListener\CustomValidationListener:
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onValidationException }
public function onValidationException(GetResponseForExceptionEvent $event) {
$exception = $event->getThrowable();
if ($exception instanceof ValidationFailedException) {
$event->setResponse(new JsonResponse([
'errors' => $this->formatErrors($exception->getErrors()),
], 422));
}
}
Dynamic Constraints:
Callback constraints to add runtime logic:
new Callback(function ($value) {
return $value === 'admin' ? 'Admin role is restricted' : null;
})
Validation Groups:
create, update) and validate selectively:
new GroupSequence(['create' => ['name', 'email']]);
Custom Validators:
Symfony\Component\Validator\ConstraintValidator:
class UniqueEmailValidator extends ConstraintValidator {
public function validate($value, Constraint $constraint) {
if (User::where('email', $value)->exists()) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Then use it in constraints:
new UniqueEmail(['message' => 'Email already exists.'])
How can I help you explore Laravel packages today?