Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6f85f72

Browse filesBrowse files
feature #49138 [HttpKernel] Create Attributes #[MapRequestPayload] and #[MapQueryString] to map Request input to typed objects (Koc)
This PR was merged into the 6.3 branch. Discussion ---------- [HttpKernel] Create Attributes `#[MapRequestPayload]` and `#[MapQueryString]` to map Request input to typed objects | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #36037, #36093, #45628, #47425, #49002, #49134 | License | MIT | Doc PR | TBD Yet another variation of how we can map raw Request data to typed objects with validation. We can even build OpenApi Specification based on this DTO classes using [NelmioApiDocBundle](https://github.com/nelmio/NelmioApiDocBundle). ## Usage Example 🔧 ### `#[MapRequestPayload]` ```php class PostProductReviewPayload { public function __construct( #[Assert\NotBlank] #[Assert\Length(min: 10, max: 500)] public readonly string $comment, #[Assert\GreaterThanOrEqual(1)] #[Assert\LessThanOrEqual(5)] public readonly int $rating, ) { } } class PostJsonApiController { public function __invoke( #[MapRequestPayload] PostProductReviewPayload $payload, ): Response { // $payload is validated and fully typed representation of the request body $request->getContent() // or $request->request->all() } } ``` ### `#[MapQueryString]` ```php class GetOrdersQuery { public function __construct( #[Assert\Valid] public readonly ?GetOrdersFilter $filter, #[Assert\LessThanOrEqual(500)] public readonly int $limit = 25, #[Assert\LessThanOrEqual(10_000)] public readonly int $offset = 0, ) { } } class GetOrdersFilter { public function __construct( #[Assert\Choice(['placed', 'shipped', 'delivered'])] public readonly ?string $status, public readonly ?float $total, ) { } } class GetApiController { public function __invoke( #[MapQueryString] GetOrdersQuery $query, ): Response { // $query is validated and fully typed representation of the query string $request->query->all() } } ``` ### Exception handling 💥 - Validation errors will result in an HTTP 422 response, accompanied by a serialized `ConstraintViolationList`. - Malformed data will be responded to with an HTTP 400 error. - Unsupported deserialization formats will be responded to with an HTTP 415 error. ## Comparison to another implementations 📑 Differences to #49002: - separate Attributes for explicit definition of the used source - no need to define which class use to map because Attributes already associated with typed argument - used ArgumentValueResolver mechanism instead of Subscribers - functional tests Differences to #49134: - it is possible to map whole query string to object, not per parameter - there is validation of the mapped object - supports both `$request->getContent()` and `$request->request->all()` mapping - functional tests Differences to #45628: - separate Attributes for explicit definition of the used source - supports `$request->request->all()` and `$request->query->all()` mapping - Exception handling opt-in - functional tests ## Bonus part 🎁 - Extracted `UnsupportedFormatException` which thrown when there is no decoder for a given format Commits ------- d987093 [HttpKernel] Create Attributes `#[MapRequestPayload]` and `#[MapQueryString]` to map Request input to typed objects
2 parents 3345ee3 + d987093 commit 6f85f72
Copy full SHA for 6f85f72

File tree

17 files changed

+851
-7
lines changed
Filter options

17 files changed

+851
-7
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class UnusedTagsPass implements CompilerPassInterface
4646
'container.stack',
4747
'controller.argument_value_resolver',
4848
'controller.service_arguments',
49+
'controller.targeted_value_resolver',
4950
'data_collector',
5051
'event_dispatcher.dispatcher',
5152
'form.type',

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+10-1Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
158158
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
159159
use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer;
160+
use Symfony\Component\Serializer\Serializer;
160161
use Symfony\Component\Serializer\SerializerAwareInterface;
161162
use Symfony\Component\Stopwatch\Stopwatch;
162163
use Symfony\Component\String\LazyString;
@@ -357,11 +358,19 @@ public function load(array $configs, ContainerBuilder $container)
357358
$container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);
358359

359360
if ($this->readConfigEnabled('serializer', $container, $config['serializer'])) {
360-
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
361+
if (!class_exists(Serializer::class)) {
361362
throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".');
362363
}
363364

364365
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
366+
} else {
367+
$container->register('.argument_resolver.request_payload.no_serializer', Serializer::class)
368+
->addError('You can neither use "#[MapRequestPayload]" nor "#[MapQueryString]" since the Serializer component is not '
369+
.(class_exists(Serializer::class) ? 'enabled. Try setting "framework.serializer" to true.' : 'installed. Try running "composer require symfony/serializer-pack".')
370+
);
371+
372+
$container->getDefinition('argument_resolver.request_payload')
373+
->replaceArgument(0, new Reference('.argument_resolver.request_payload.no_serializer', ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE));
365374
}
366375

367376
if ($propertyInfoEnabled) {

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
1818
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
1919
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
20+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
2021
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
2122
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver;
2223
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver;
@@ -61,6 +62,13 @@
6162
])
6263
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => DateTimeValueResolver::class])
6364

65+
->set('argument_resolver.request_payload', RequestPayloadValueResolver::class)
66+
->args([
67+
service('serializer'),
68+
service('validator')->nullOnInvalid(),
69+
])
70+
->tag('controller.targeted_value_resolver', ['name' => RequestPayloadValueResolver::class])
71+
6472
->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
6573
->tag('controller.argument_value_resolver', ['priority' => 100, 'name' => RequestAttributeValueResolver::class])
6674

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.