baptiste-contreras/symfony-request-param-bundle
Install the Bundle
composer require baptiste-contreras/symfony-request-param-bundle
Register the bundle in config/bundles.php:
return [
// ...
BaptisteContreras\SymfonyRequestParamBundle\SymfonyRequestParamBundle::class => ['all' => true],
];
Define a DTO Class
Create a DTO class (e.g., RegisterRequest) with validation rules (e.g., using Symfony Validator):
use Symfony\Component\Validator\Constraints as Assert;
class RegisterRequest
{
#[Assert\NotBlank]
public string $name;
#[Assert\Email]
public string $email;
}
Annotate Controller Action
Use #[DtoRequestParam] on the DTO parameter in your controller:
use BaptisteContreras\SymfonyRequestParamBundle\Attribute\DtoRequestParam;
#[Route('/register', name: 'register', methods: ['POST'])]
public function register(#[DtoRequestParam] RegisterRequest $request): Response
{
// $request is now populated and validated
}
Enable Auto-Provision (Optional)
Add #[AutoProvideRequestDto] to the controller to auto-provision DTOs for all actions:
#[AutoProvideRequestDto]
class RegisterController extends AbstractController
{
// ...
}
Replace manual JSON decoding and validation with a single DTO parameter:
#[Route('/user', name: 'user_create', methods: ['POST'])]
public function createUser(#[DtoRequestParam] UserDto $userDto): Response
{
// $userDto is ready for use (e.g., $userDto->name, $userDto->email)
}
Request Body:
{
"name": "John Doe",
"email": "john@example.com"
}
Replace Manual Parsing: Eliminate repetitive code for JSON/XML/form data parsing and validation.
// Before:
$data = json_decode($request->getContent(), true);
$validator = new Validator();
$errors = $validator->validate($data, $constraints);
// After:
public function submit(#[DtoRequestParam] SubmitDto $dto): Response
{
// $dto is validated and populated
}
Reuse DTOs Across Controllers:
// UserController.php
public function update(#[DtoRequestParam] UserDto $dto): Response { ... }
// AdminController.php
public function bulkUpdate(#[DtoRequestParam] array $dtos): Response { ... }
Leverage sourceType to handle different input formats (default: SourceType::JSON):
// Query Parameters
#[DtoRequestParam(sourceType: SourceType::QUERY)]
public function search(SearchDto $dto): Response { ... }
// Form Data
#[DtoRequestParam(sourceType: SourceType::FORM)]
public function createFromForm(FormDto $dto): Response { ... }
Note: Ensure a DtoProviderDriverInterface exists for custom sourceType (e.g., SourceType::XML).
Group Validation:
#[DtoRequestParam(validateDto: true, validationGroups: ['create'])]
public function create(CreateDto $dto): Response { ... }
Define groups in your DTO:
class CreateDto {
#[Assert\NotBlank(groups: ['create'])]
public string $field;
}
Disable Validation:
#[DtoRequestParam(validateDto: false)]
public function debug(DebugDto $dto): Response { ... }
Silent Failure:
#[DtoRequestParam(throwDeserializationException: false)]
public function safeUpdate(SafeDto $dto): Response { ... }
Returns null on failure (check with null !== $dto).
Custom Error Responses:
Use Symfony’s ExceptionListener to handle DeserializationException globally.
Mock DTOs in tests:
public function testRegister(): void
{
$dto = new RegisterRequest();
$dto->name = 'Test';
$dto->email = 'test@example.com';
$this->container->get('request_stack')->push(
Request::create('/register', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($dto))
);
$this->client->request('POST', '/register');
// Assert response...
}
Missing Drivers:
NoDtoProviderDriverFoundException for unsupported sourceType.DtoProviderDriverInterface or stick to default types (JSON, QUERY, FORM).Circular References:
User ↔ Profile) may fail silently or throw exceptions.#[Assert\Valid] sparingly or flatten nested objects.Case Sensitivity:
#[Assert\Property] or a custom DtoProviderDriver for case-insensitive mapping.Validation Overhead:
validateDto: false) bypasses Symfony’s validator.#[Assert\Callback] for complex validation logic.Check Deserialization: Enable debug mode to see raw request data:
#[DtoRequestParam(throwDeserializationException: true)]
public function debug(DebugDto $dto): Response { ... }
Catch DeserializationException to inspect failed data:
try {
$this->register($dto);
} catch (DeserializationException $e) {
dd($e->getFailedData()); // Raw input that failed
}
Log Validation Errors:
Use Symfony’s Validator component to log errors:
$errors = $validator->validate($dto);
foreach ($errors as $error) {
$this->logger->error($error->getMessage());
}
Bundle Auto-Registration:
// config/packages/symfony_request_param.yaml
symfony_request_param:
auto_provide: true # Optional: enable globally
Priority in Dependency Injection:
// Bad: Constructor modifies static state
class BadDto {
public function __construct() { StaticService::setContext('bad'); }
}
// Good: Lazy initialization
class GoodDto {
private ?Context $context = null;
}
Custom Source Types:
Implement DtoProviderDriverInterface for new formats (e.g., SourceType::GRAPHQL):
class GraphQLDriver implements DtoProviderDriverInterface
{
public function provideDtoFromSource(
string $sourceType,
mixed $data,
string $dtoClass,
array $options
): object {
// Parse GraphQL input into $dtoClass
}
}
Register the driver in the bundle’s services:
# config/services.yaml
BaptisteContreras\SymfonyRequestParamBundle\DtoProviderDriverInterface:
tags: [symfony_request_param.driver]
Override Default Behavior:
Extend DtoRequestParam attributes or use middleware to pre-process requests:
// src/EventListener/RequestListener.php
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
if ($request->isMethod('POST') && $request->getPathInfo() === '/custom') {
$request->attributes->set('custom_dto', new CustomDto());
}
}
Integrate with API Platform:
Use #[DtoRequestParam] alongside #[ApiResource] for hybrid validation:
#[ApiResource]
#[DtoRequestParam(sourceType: SourceType::JSON)]
class PostDto extends AbstractDto
{
#[Assert\Length(min: 10)]
public string $content;
}
# config/packages/validator.yaml
How can I help you explore Laravel packages today?