Description
Description
Motivation
Symfony provides attributes like #[MapRequestPayload]
, #[MapQueryParameter]
, and #[MapUploadedFile]
to conveniently map request data to typed objects (DTOs) in controller actions. These attributes are resolved using Symfony’s argument value resolvers and provide a powerful declarative experience — including automatic validation, format handling, and integration with the Serializer and Validator components.
However, this functionality is currently limited to controller arguments. In many real-world applications, developers frequently need to map and validate request data outside of controllers — for example:
- In custom authenticators
- In event listeners or middleware
- In reusable services handling Request objects
Currently, developers must reimplement mapping and validation logic manually in these contexts, leading to duplicated effort, inconsistent behavior, and poor DX (developer experience).
Goal
Enable the same mapping and validation behavior available via #[MapRequestPayload]
, #[MapQueryParameter]
, and #[MapUploadedFile]
in any Symfony service, not just controller actions.
Proposal
-
Extract the core logic of the existing attribute-based mapping system into reusable internal services:
- PayloadMapper — handles deserialization of JSON/XML/etc. from the request body
- QueryParameterMapper — maps query string data into DTOs
- UploadedFileMapper — extracts and maps uploaded files
These will encapsulate the transformation logic without coupling to the controller resolver system.
-
Introduce a high-level public API: RequestDataMapper
This service:- Combines mapping and validation
- Uses the mappers above
- Reproduces the exact behavior of controller argument resolution
- Throws ValidationFailedException on constraint violations
- Designed for use in custom authenticators, listeners, services, etc.
Example usage:
$dto = $this->requestDataMapper->mapAndValidatePayload($request, LoginRequest::class);
Design
Low-level mappers (internal services)
- PayloadMapper::map(Request $request, string $class): object
- QueryParameterMapper::map(Request $request, string $class): object
- UploadedFileMapper::map(Request $request, string $field): ?UploadedFile
These services handle extraction and transformation only. No validation logic.
High-level public service
final class RequestDataMapper
{
public function __construct(
private PayloadMapper $payloadMapper,
private QueryParameterMapper $queryMapper,
private UploadedFileMapper $fileMapper,
private ValidatorInterface $validator
) {}
public function mapAndValidatePayload(Request $request, string $class, array $serializerContext = [], array|string|null $validationGroups = null, ?string $format = null): object;
public function mapAndValidateQuery(Request $request, string $class): object;
public function mapUploadedFile(Request $request, string $field): ?UploadedFile;
}
Integration
- No changes to controller attribute behavior. The #[MapRequestPayload] and other attributes will continue to use the current value resolver mechanism.
- Internally, these resolvers will be refactored to delegate to the new mappers, avoiding duplicated logic and ensuring consistency.
- Developers can inject RequestDataMapper into any service via autowiring.
Backward Compatibility
- This change is fully backward-compatible.
- All existing controller attribute behavior remains unchanged.
- The new mappers and RequestDataMapper are additive and opt-in.
Implementation Plan
If accepted, I propose to:
-
Extract existing logic from the controller value resolvers into three private mappers:
- PayloadMapper
- QueryParameterMapper
- UploadedFileMapper
-
Introduce a new public service RequestDataMapper that:
- Uses the above
- Handles validation
- Mirrors controller behavior
-
Update the current value resolvers to delegate to the new services
-
Add functional and unit tests for:
- Each mapper individually
- Combined RequestDataMapper usage
- Exception handling and validation
-
Update Symfony documentation (if needed) with examples for RequestDataMapper.
Conclusion
This RFC proposes a clean, fully backward-compatible enhancement to Symfony's request handling model. It extracts and elevates the powerful attribute-based mapping logic into reusable, injectable services — aligning with Symfony’s philosophy of modularity and DX excellence.
It closes a common gap experienced by developers working with authenticators, listeners, and service-level request data access.
Example
Use Cases
- Authenticators
public function authenticate(Request $request): PassportInterface
{
$dto = $this->requestDataMapper->mapAndValidatePayload($request, LoginRequest::class);
return new Passport(
new UserBadge($dto->username),
new PasswordCredentials($dto->password)
);
}
- Event Subscribers
public function onKernelRequest(RequestEvent $event): void
{
$filter = $this->requestDataMapper->mapAndValidateQuery($event->getRequest(), FilterDto::class);
// ...
}
- Services
$dto = $this->requestDataMapper->mapAndValidatePayload($request, SearchCriteria::class);