crtl / request-dto-resolver-bundle
Deserializes and validates requests into objects
Installs: 85
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 1
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
README
A Symfony bundle for predictable, type-safe instantiation and validation of request DTOs.
It removes boilerplate from controllers while staying close to Symfony’s native validation and argument resolving mechanisms.
Features
-
Automatic DTO Resolution
DTOs type-hinted in controller actions are instantiated and validated automatically. -
Native Symfony Validator Integration
Uses Symfony’sValidatorInterfacewithout custom validation layers. -
Nested DTO Support
Supports complex request payloads with nested DTOs for query, body, header, file and route parameters. -
Strict Typing Friendly
DTO properties can be strictly typed for better IDE support and safer refactoring. -
Flexible Query Parameter Transformation
Query parameters can be transformed to scalar types or via custom callbacks.
Installation
composer require crtl/request-dto-resolver-bundle
Configuration
Register the bundle in your Symfony application:
// config/bundles.php return [ // ... Crtl\RequestDtoResolverBundle\CrtlRequestDtoResolverBundle::class => ["all" => true], ];
Bundle Options
The bundle exposes two options under the crtl_request_dto_resolver key:
# config/packages/crtl_request_dto_resolver.yaml crtl_request_dto_resolver: default_strict: true # default: true default_null: false # default: false
| Option | Type | Default | Description |
|---|---|---|---|
default_strict |
bool |
true |
Controls how property values are assigned during hydration. When true, values are assigned directly ($object->prop = $value), which enforces PHP's native type checks. When false, values are assigned via reflection, allowing implicit type coercion. |
default_null |
bool |
false |
When true, properties that are missing from the request are treated as if null was sent. For non-nullable properties this produces a type constraint violation; for nullable properties the value is skipped as usual. |
These serve as bundle-level defaults. Individual DTOs can override them via the #[RequestDto] attribute:
// Uses bundle defaults for both options #[RequestDto] class MyDto { /* ... */ } // Overrides strict for this DTO only #[RequestDto(strict: false)] class NonStrictDto { /* ... */ } // Overrides defaultNull for this DTO only #[RequestDto(defaultNull: true)] class RequireAllFieldsDto { /* ... */ }
When the attribute parameter is omitted (or explicitly set to null), the bundle-level default is used.
Usage
Step 1: Define a Request DTO
Create a DTO class and annotate it with #[RequestDto].
Use parameter attributes to map request data to properties.
The attribute is required to identify which controller arguments should be resolved and validated.
1.1 Strictly typed DTO
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\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; #[RequestDto] class ExampleDto { #[BodyParam, Assert\NotBlank, Assert\Type("string")] public string $someParam; #[BodyParam, Assert\NotBlank, Assert\Type("string")] public string $withDefaultValue = "My default value"; #[BodyParam] public ?string $nullable; #[FileParam, Assert\NotNull] public ?UploadedFile $file; #[HeaderParam("Content-Type"), Assert\NotBlank] public string $contentType; // Because query params are all strings by default // we can provide a type transformer to transform its type. // values are converted using filter_var with the corrosponding FILTER_VALIDATE_* option. #[QueryParam(transformType: "int"), Assert\GreaterThan(18)] public int $age; // Or provide a custom callable to tranform type #[ QueryParam( name: "age", transformType: fn(string $value) => strtolower($value) ), Assert\GreaterThan(18) ] public mixed $customQueryParam; #[RouteParam, Assert\NotBlank] public string $id; // Nested DTOs are supported for BodyParam and QueryParam // Do NOT use Assert\Valid here #[BodyParam("nested")] public ?NestedRequestDTO $nestedBodyDto; // Optional constructor receiving the Request // It is recommended to make the request argument nullable to support creation of DTOs from // array data but not required when only used in HTTP contexts. public function __construct(?Request $request = null) { } }
Any type mismatches will trigger a constraint violation and thus a
RequestValidationExceptionis thrown.
1.2 Mixed typed DTO
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\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Validator\Constraints as Assert; #[RequestDto] class ExampleDto { /** * @var string */ #[BodyParam, Assert\NotBlank] public mixed $contentType; }
1.3 Important notes about DTO hydration
-
Uninitialized properties
If a request does not contain data for a property, that property will remain uninitialized.
To guarantee initialization, either:- provide a default value, or
- add appropriate validation constraints (e.g.
NotNull,NotBlank).
-
Hydration from arrays
When a DTO is manually hydrated from an array usingRequestDtoFactory::fromArray(), the configuredAbstractParam::$nameis ignored.
In this case, the DTO’s property names are always used as the source keys. -
Validation still runs in strict mode
Even if some properties cannot be assigned due to type mismatches in strict typing mode, the DTO is still fully validated using Symfony’s validator.
Any resulting violations will lead to aRequestValidationException.
1.4 Validation group sequences
All Symfony validation group sequence variants are supported.
Because request data can never be trusted, DTO properties may be uninitialized regardless of whether strict typing is used or not.
Missing or invalid input can prevent a property from being assigned during hydration.
When using validation group sequences, you must therefore ensure that properties are accessed safely by:
- checking initialization with
isset(), or - using reflection-based checks when necessary.
Failing to do so may lead to runtime errors before later validation groups are evaluated.
Step 2: Use the DTO in a Controller
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 { return new Response("DTO received and validated successfully!"); } }
Handling Validation Errors
On validation failure, a RequestValidationException is thrown.
The bundle registers a default exception subscriber (priority -32) that returns a
400 Bad RequestJSON response.
You can override this with your own listener:
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 [ KernelEvents::EXCEPTION => ["onKernelException", 0], ]; } public function onKernelException(ExceptionEvent $event): void { $exception = $event->getThrowable(); if ($exception instanceof RequestValidationException) { $event->setResponse(new JsonResponse([ "error" => "Validation failed", "details" => $exception->getViolations(), ], JsonResponse::HTTP_BAD_REQUEST)); } } }
License
This bundle is licensed under the MIT License. See the LICENSE file for details.