crtl / request-dto-resolver-bundle
Deserializes and validates requests into objects
Installs: 42
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 2
Type:symfony-bundle
pkg:composer/crtl/request-dto-resolver-bundle
Requires
- php: >=8.2
- phpdocumentor/reflection-docblock: ^5.6
- psr/log: ^3.0
- symfony/cache: ^7.1 | ^8.0
- symfony/dependency-injection: ^7.2 | ^8.0
- symfony/framework-bundle: ^7.1 | ^8.0
- symfony/http-kernel: ^7.2 | ^8.0
- symfony/property-info: ^7.2 | ^8.0
- symfony/validator: ^7.2 | ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^v3.93.1
- jaschilz/php-coverage-badger: ^2.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^10
- symfony/phpunit-bridge: ^7.1 | ^8.0
Conflicts
- symfony/dependency-injection: >=9.0
- symfony/http-kernel: >=9.0
- symfony/validator: >=9.0
This package is auto-updated.
Last update: 2026-02-04 23:05:45 UTC
README
Symfony bundle for streamlined instantiation and validation of request DTOs.
Features
- Automatic DTO Handling:
Instantly creates and validates Data Transfer Objects (DTOs) fromRequestdata, 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. - Strict Typing Support:
DTO properties can now be strictly typed, ensuring better code quality and IDE support. - Flexible Query Transformation:
Built-in support for transforming query parameters into specific types (int, float, string, bool) or via custom callbacks.
Installation
composer require crtl/request-dto-resolver-bundle
Configuration
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], ];
Usage
Step 1: Create a DTO
Create a class to represent your request data.
Annotate the class with #[RequestDto] and use the attributes below 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 Crtl\RequestDtoResolverBundle\Attribute\RequestDto; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; #[RequestDto] class ExampleDTO { // DTOs can now be strictly typed. // Important: Validation constraints must be correct to prevent TypeErrors // since hydration happens after validation. #[BodyParam, Assert\NotBlank, Assert\Type("string")] 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; // QueryParam supports optional transformType: "int", "float", "string", "bool" // or a custom callback: fn(string $val) => ... #[QueryParam(name: "age", transformType: "int"), Assert\GreaterThan(18)] public int $age; // Matches id #[RouteParam, Assert\NotBlank] public string $id; // Nested DTOs are supported for BodyParam and QueryParam #[BodyParam("nested")] // Dont use Assert\Valid on nested DTOs otherwise native validation is triggered public ?NestedRequestDTO $nestedBodyDto; // Optionally implement constructor which accepts request object // Only the request is passed; properties are NOT yet initialized here. public function __construct(Request $request) { } }
IMPORTANT: Strict Typing
While strict types are supported, validation constraint mismatches can still lead toTypeErrorin production. Always ensure your constraints (e.g.,Assert\Type,Assert\NotBlank) match your property types.
IMPORTANT: Nested DTOs
Dont use any assertions on nested DTO properties as this will trigger the native validation fow eventually breaking hydration and triggering errors.
DTO Lifecycle
- Resolution:
RequestDtoResolverinstantiates the DTO during the controller argument resolving phase. Only theRequestobject is passed to the constructor. - Security: Symfony security checks (e.g.,
#[IsGranted]) are executed. - Validation & Hydration: An event subscriber (
RequestDtoValidationEventSubscriber) listens tokernel.controller_arguments. It validates the DTO data and, if successful, hydrates the DTO properties.
Step 2: Use the DTO in a Controller
Inject the DTO into your controller action. It will be automatically instantiated, validated, and hydrated.
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 and hydrated request data return new Response("DTO received and validated successfully!"); } }
Using RequestDtoTrait
The RequestDtoTrait provides a default constructor that accepts the Request object and a getValue(string $property) method. This method is useful for accessing request data before hydration, which can come in handy in group sequence providers.
use Crtl\RequestDtoResolverBundle\Attribute\RequestDto; use Crtl\RequestDtoResolverBundle\Trait\RequestDtoTrait; use Crtl\RequestDtoResolverBundle\Attribute\BodyParam; #[RequestDto] class MyDTO { use RequestDtoTrait; #[BodyParam] public string $type; }
Validation Group Sequences
When using Group Sequences to define conditional validation, you must be careful about how you access data.
IMPORTANT: Uninitialized Properties
Since hydration happens after validation, DTO properties are uninitialized when the group sequence is evaluated. Accessing them directly will throw an Error.
To safely access request parameters in your group sequence logic, use RequestDtoTrait::getValue():
use Crtl\RequestDtoResolverBundle\Attribute\RequestDto; use Crtl\RequestDtoResolverBundle\Trait\RequestDtoTrait; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\GroupSequenceProviderInterface; #[RequestDto] class MyDTO implements GroupSequenceProviderInterface { use RequestDtoTrait; #[BodyParam] public string $type; public function getGroupSequence(): array|GroupSequence { // Use getValue() instead of $this->type $type = $this->getValue("type"); $groups = ["MyDTO"]; if ($type === "special") { $groups[] = "Special"; } return $groups; } }
Using a Group Sequence Provider Service
You can also use a service to provide the group sequence. This is useful if your validation logic depends on external services (e.g., a database or configuration).
- Create the Provider Service:
namespace App\Validator; use App\DTO\MyDTO; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\GroupProviderInterface; class MyGroupSequenceProvider implements GroupProviderInterface { public function getGroups(object $object): array|GroupSequence { assert($object instanceof MyDTO) $groups = ["MyDTO"]; // Use getValue() to safely access uninitialized properties if ($object->getValue("type") === "special") { $groups[] = "Special"; } return $groups; } }
- Configure the DTO:
use App\Validator\MyGroupSequenceProvider; use Crtl\RequestDtoResolverBundle\Attribute\RequestDto; use Crtl\RequestDtoResolverBundle\Trait\RequestDtoTrait; use Symfony\Component\Validator\Constraints as Assert; #[RequestDto] #[Assert\GroupSequenceProvider(provider: MyGroupSequenceProvider::class)] class MyDTO { // Trait is important to access fields before validation use RequestDtoTrait; // ... }
Note:
getValue()only works in root DTOs. It uses reflection to resolve data from the request, which cannot access parent data in nested contexts.
Step 3: Handle Validation Errors
When validation fails, a Crtl\RequestDtoResolverBundle\Exception\RequestValidationException is thrown.
The bundle registers a default exception subscriber (RequestValidationExceptionEventSubscriber) with a low priority of -32. This ensures that validation exceptions are caught and converted into a JsonResponse with a 400 Bad Request status code by default.
You can still provide your own listener if you need custom error formatting:
namespace App\EventListener; use Crtl\RequestDtoResolverBundle\Exception\RequestValidationException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; class RequestValidationExceptionListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ // Use a priority > -32 to override the default bundle subscriber KernelEvents::EXCEPTION => ["onKernelException", 0], ]; } public function onKernelException(ExceptionEvent $event): void { $exception = $event->getThrowable(); if ($exception instanceof RequestValidationException) { $response = new JsonResponse([ "error" => "Validation failed", "details" => $exception->getViolations(), ], JsonResponse::HTTP_BAD_REQUEST); $event->setResponse($response); } } }
License
This bundle is licensed under the MIT License. See the LICENSE file for more details.