crtl/request-dto-resolver-bundle
composer require crtl/request-dto-resolver-bundle
config/bundles.php:
Crtl\RequestDtoResolverBundle\CrtlRequestDtoResolverBundle::class => ["all" => true],
#[RequestDto] and parameter attributes (e.g., #[BodyParam], #[QueryParam]):
#[RequestDto]
class CreateUserDto {
#[BodyParam, Assert\NotBlank]
public string $name;
}
#[Route('/users', methods: ['POST'])]
public function createUser(CreateUserDto $dto): Response { ... }
Replace a manual request validation workflow (e.g., Request::get() + ValidatorInterface) with a single DTO argument. For example:
// Before (manual)
public function createUser(Request $request, ValidatorInterface $validator): Response {
$data = $request->request->all();
$errors = $validator->validate($data, new CreateUserConstraints());
if (count($errors)) { ... }
// Process $data
}
// After (DTO)
public function createUser(CreateUserDto $dto): Response {
// $dto is already validated and hydrated
return new Response("User created: {$dto->name}");
}
Strict vs. Loose Typing:
Use #[RequestDto(strict: false)] for DTOs where implicit type coercion is needed (e.g., legacy APIs). Default is true (bundle/config level).
#[RequestDto(strict: false)]
class LegacyDto {
#[BodyParam]
public mixed $age; // Accepts "25" as int
}
Nested DTOs:
Support complex payloads with nested structures. Avoid Assert\Valid—use the bundle’s native resolution:
#[RequestDto]
class UserProfileDto {
#[BodyParam("address")]
public AddressDto $address;
}
#[RequestDto]
class AddressDto {
#[BodyParam]
public string $street;
}
Query Parameter Transformation:
Convert query strings to typed values using transformType or custom callbacks:
#[QueryParam(transformType: "int")] // Uses FILTER_VALIDATE_INT
public int $page;
#[QueryParam(transformType: fn(string $v) => (int) $v * 2)]
public int $multiplier;
Single DTO per Action: Ideal for simple APIs where one DTO maps to the entire request payload.
public function updateUser(UpdateUserDto $dto): Response { ... }
Multiple DTOs: Combine DTOs for different parts of the request (e.g., query + body):
public function searchUsers(
SearchQueryDto $query,
FilterDto $filter
): Response { ... }
Conditional Validation: Use Symfony’s validation groups to validate DTOs differently based on context:
#[RequestDto]
class UserDto {
#[BodyParam, Assert\NotBlank(groups: ["create"])]
public string $password;
}
// In controller:
$validator->validate($dto, null, ["create"]);
Event Listeners:
Override the default RequestValidationException handler to customize error responses:
// config/services.yaml
App\EventListener\RequestValidationListener:
tags:
- { name: kernel.event_subscriber }
Dependency Injection:
Inject RequestDtoFactory for manual DTO creation (e.g., in services):
public function __construct(
private RequestDtoFactory $dtoFactory
) {}
public function process(array $data): void {
$dto = $this->dtoFactory->fromArray(MyDto::class, $data);
}
API Platform Integration:
Use DTOs alongside API Platform’s #[ApiResource] for hybrid validation:
#[ApiResource, RequestDto]
class Book {
#[BodyParam, Assert\NotBlank]
public string $title;
}
Unit Testing DTOs: Test hydration and validation in isolation:
public function testDtoHydration(): void {
$dto = $this->dtoFactory->fromArray(
CreateUserDto::class,
["name" => "John"]
);
$this->assertEquals("John", $dto->name);
}
Controller Testing:
Use RequestDtoFactory to create DTOs for controller tests:
public function testCreateUser(): void {
$dto = $this->dtoFactory->fromArray(CreateUserDto::class, ["name" => "Jane"]);
$response = $this->client->postJson("/users", $dto->toArray());
$this->assertResponseIsSuccessful();
}
Missing #[RequestDto] Attribute:
#[RequestDto].Strict Mode Type Mismatches:
strict: true), type mismatches (e.g., string → int) throw RequestValidationException during hydration.strict: false for loose typing.25 as "25" for int fields).Uninitialized Properties:
null if missing in the request, causing UndefinedPropertyException in validation groups.public ?string $optional = null;.Assert\NotNull or Assert\NotBlank to enforce presence.isset($dto->property) ? $dto->property : ....Circular References in Nested DTOs:
A contains B, B contains A) may cause infinite loops.Query Parameter Name Conflicts:
#[QueryParam] fields map to the same query parameter name, the last one wins.#[QueryParam(name: "user_id")]
public int $id;
File Uploads:
UploadedFile properties require #[FileParam] and may fail if the file key doesn’t match the property name.name in #[FileParam] to specify the form field name:
#[FileParam(name: "avatar")]
public ?UploadedFile $image;
Enable Debug Mode:
Inspect Violations:
RequestValidationException to log or inspect violations:
try {
$this->controller->action($dto);
} catch (RequestValidationException $e) {
error_log($e->getViolations());
}
Check Hydration Order:
defaultNull: true to skip missing properties.Validate DTOs Manually:
ValidatorInterface to validate DTOs outside controllers:
$validator->validate($dto);
Bundle-Level Defaults:
default_strict: false in config overrides all DTOs unless explicitly set.default_null: true treats missing properties as null (useful for optional fields).Attribute Precedence:
#[RequestDto(strict: false)]) override bundle/config defaults.Performance:
php bin/console cache:clear
Custom Param Attributes:
#[CookieParam]) by implementing ParamAttributeInterface.Custom Hydrators:
How can I help you explore Laravel packages today?