Symfony bundle for streamlined instantiation and validation of request DTOs.
- Automatic DTO Handling:
Instantly creates and validates Data Transfer Objects (DTOs) fromRequest
data, that are type-hinted in controller actions. - Symfony Validator Integration:
Leverages Symfony's built-in validator to ensure data integrity and compliance with your validation rules. - Nested DTO Support:
Handles complex request structures by supporting nested DTOs for both query and body parameters, making it easier to manage hierarchical data.
composer crtl/request-dto-resolver-bundle
Register the bundle in your Symfony application. Add the following to your config/bundles.php
file:
return [
// other bundles
Crtl\RequestDTOResolverBundle\CrtlRequestDTOResolverBundle::class => ["all" => true],
];
Create a class to represent your request data.
Annotate the class with #[RequestDTO]
and use bellow attributes for properties to map request parameters.
namespace App\DTO;
use Crtl\RequestDTOResolverBundle\Attribute\BodyParam;
use Crtl\RequestDTOResolverBundle\Attribute\FileParam;
use Crtl\RequestDTOResolverBundle\Attribute\HeaderParam;
use Crtl\RequestDTOResolverBundle\Attribute\QueryParam;
use Crtl\RequestDTOResolverBundle\Attribute\RouteParam;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraints as Assert;
#[RequestDTO]
class ExampleDTO
{
// Matches someParam in request body
#[BodyParam, Assert\NotBlank]
public ?string $someParam;
// Matches file in uploaded files
#[FileParam, Assert\NotNull]
public mixed $file;
// Matches Content-Type header in headers
#[HeaderParam("Content-Type"), Assert\NotBlank]
public string $contentType;
// Pass string to param if property does not match param name.
// Matches queryParamName in query params
#[QueryParam("queryParamName"), Assert\NotBlank]
public string $query;
// Matches id
#[RouteParam, Assert\NotBlank]
public string $id;
// Nested DTOs are supported for BodyParam and QueryParam
#[BodyParam("nested"), Assert\Valid]
public ?NestedRequestDTO $nestedBodyDto;
#[QueryParam("nested")]
public ?NestedRequestDTO $nestedQueryParamDto;
// Optionally implement constructor which accepts request object
public function __construct(Request $request) {
}
}
IMPORTANT
Each property must accept the type?string
except properties that are request DTOs.
Otherwise PHP may throw TypeErrors at runtime because we cannot know that the request contains the valid data type before validation.
By default, each parameter is resolved by its property name.
If property name does not match parameter name you can pass an optional string to the constructor of each*Param
attribute (seeAbstractParam::__construct
).
Each DTO can define an optional constructor which accepts a
Request
object
Inject the DTO into your controller action. The RequestValueResolver
will automatically instantiate and validate the DTO.
namespace App\Controller;
use App\DTO\ExampleDTO;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ExampleController extends AbstractController
{
#[Route("/example", name: "example")]
public function exampleAction(ExampleDTO $data): Response
{
// $data is an instance of ExampleDTO with validated request data
return new Response("DTO received and validated successfully!");
}
}
When validation fails, a
Crtl\RequestDTOResolverBundle\Exception\RequestValidationException
is thrown.
You can create an event listener or override the default exception handler and handle validation errors.
namespace App\EventListener;
use Crtl\RequestDTOResolverBundle\Exception\RequestValidationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
class RequestValidationExceptionListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => "onKernelException",
];
}
public function onKernelException(ExceptionEvent $event)
{
$exception = $event->getThrowable();
if ($exception instanceof RequestValidationException) {
$response = new JsonResponse([
"error" => "Validation failed",
"details" => $exception->getViolations(),
], JsonResponse::HTTP_BAD_REQUEST);
$event->setResponse($response);
}
}
}
This bundle is licensed under the MIT License. See the LICENSE file for more details.