Skip to content

Commit 1b619a2

Browse files
committed
Create Attributes to map Query String and Request Content to typed objects
1 parent b497938 commit 1b619a2

File tree

14 files changed

+568
-1
lines changed

14 files changed

+568
-1
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ public function load(array $configs, ContainerBuilder $container)
425425
}
426426

427427
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
428+
} else {
429+
$container->removeDefinition('argument_resolver.query_string');
430+
$container->removeDefinition('argument_resolver.request_content');
428431
}
429432

430433
if ($propertyInfoEnabled) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
1717
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
1818
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
19+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\MapQueryStringValueResolver;
20+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\MapRequestContentValueResolver;
1921
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
2022
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
2123
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver;
@@ -60,6 +62,20 @@
6062
])
6163
->tag('controller.argument_value_resolver', ['priority' => 100])
6264

65+
->set('argument_resolver.query_string', MapQueryStringValueResolver::class)
66+
->args([
67+
service('serializer'),
68+
service('validator')->nullOnInvalid(),
69+
])
70+
->tag('controller.argument_value_resolver', ['priority' => 100])
71+
72+
->set('argument_resolver.request_content', MapRequestContentValueResolver::class)
73+
->args([
74+
service('serializer'),
75+
service('validator')->nullOnInvalid(),
76+
])
77+
->tag('controller.argument_value_resolver', ['priority' => 100])
78+
6379
->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
6480
->tag('controller.argument_value_resolver', ['priority' => 100])
6581

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
16+
use Symfony\Component\HttpKernel\Attribute\MapRequestContent;
17+
18+
class MappedRequestAttributesTest extends AbstractWebTestCase
19+
{
20+
public function testMapQueryString()
21+
{
22+
$client = self::createClient(['test_case' => 'MappedRequestAttributes']);
23+
24+
$client->request('GET', '/map-query-string', ['filter' => ['status' => 'approved', 'quantity' => 4]]);
25+
26+
self::assertEquals('filter.status=approved,filter.quantity=4', $client->getResponse()->getContent());
27+
}
28+
29+
public function testMapRequestContent()
30+
{
31+
$client = self::createClient(['test_case' => 'MappedRequestAttributes']);
32+
33+
$client->request(
34+
'POST',
35+
'/map-request-content',
36+
[],
37+
[],
38+
[],
39+
<<<'JSON'
40+
{
41+
"comment": "Hello everyone!"
42+
}
43+
JSON
44+
);
45+
46+
self::assertEquals('comment=Hello everyone!', $client->getResponse()->getContent());
47+
}
48+
}
49+
50+
class WithMapQueryStringController
51+
{
52+
public function __invoke(#[MapQueryString] QueryString $query): Response
53+
{
54+
return new Response("filter.status={$query->filter->status},filter.quantity={$query->filter->quantity}");
55+
}
56+
}
57+
58+
class WithMapRequestContentController
59+
{
60+
public function __invoke(#[MapRequestContent] RequestContent $content): Response
61+
{
62+
return new Response("comment={$content->comment}");
63+
}
64+
}
65+
66+
class QueryString
67+
{
68+
public function __construct(
69+
public readonly Filter $filter,
70+
) {
71+
}
72+
}
73+
74+
class Filter
75+
{
76+
public function __construct(public readonly string $status, public readonly int $quantity)
77+
{
78+
}
79+
}
80+
81+
class RequestContent
82+
{
83+
public function __construct(public readonly string $comment)
84+
{
85+
}
86+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
13+
14+
return [
15+
new FrameworkBundle(),
16+
];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
imports:
2+
- { resource: ../config/default.yml }
3+
4+
framework:
5+
serializer:
6+
enabled: true
7+
validation: true
8+
property_info: { enabled: true }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
map_query_string:
2+
path: /map-query-string
3+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapQueryStringController
4+
5+
map_request_content:
6+
path: /map-request-content
7+
controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\WithMapRequestContentController

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"symfony/error-handler": "^6.1",
2727
"symfony/event-dispatcher": "^5.4|^6.0",
2828
"symfony/http-foundation": "^6.2",
29-
"symfony/http-kernel": "^6.2.1",
29+
"symfony/http-kernel": "^6.3",
3030
"symfony/polyfill-mbstring": "~1.0",
3131
"symfony/filesystem": "^5.4|^6.0",
3232
"symfony/finder": "^5.4|^6.0",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
/**
15+
* Controller parameter tag to map Query String to typed object and validate it.
16+
*/
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapQueryString
19+
{
20+
public function __construct(public readonly array $context = [])
21+
{
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Attribute;
13+
14+
/**
15+
* Controller parameter tag to map Request Content to typed object and validate it.
16+
*/
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapRequestContent
19+
{
20+
public function __construct(public readonly string $format = 'json', public readonly array $context = [])
21+
{
22+
}
23+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
17+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
20+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
21+
use Symfony\Component\Validator\Exception\ValidationFailedException;
22+
use Symfony\Component\Validator\Validator\ValidatorInterface;
23+
24+
final class MapQueryStringValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface
25+
{
26+
private const CONTEXT = [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true];
27+
28+
public function __construct(
29+
private readonly DenormalizerInterface $normalizer,
30+
private readonly ?ValidatorInterface $validator,
31+
) {
32+
}
33+
34+
/**
35+
* @deprecated since Symfony 6.2, use resolve() instead
36+
*/
37+
public function supports(Request $request, ArgumentMetadata $argument): bool
38+
{
39+
@trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__);
40+
41+
return 1 === \count($argument->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF));
42+
}
43+
44+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
45+
{
46+
$attributes = $argument->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF);
47+
48+
if (!$attributes) {
49+
return [];
50+
}
51+
52+
/** @var MapQueryString $attribute */
53+
$attribute = $attributes[0];
54+
55+
$type = $argument->getType();
56+
if (!$type) {
57+
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->getName()));
58+
}
59+
60+
$payload = $this->normalizer->denormalize(
61+
$request->query->all(),
62+
$type,
63+
'json',
64+
$attribute->context + self::CONTEXT
65+
);
66+
67+
if ($this->validator) {
68+
$violations = $this->validator->validate($payload);
69+
70+
if (\count($violations)) {
71+
throw new ValidationFailedException($payload, $violations);
72+
}
73+
}
74+
75+
return [$payload];
76+
}
77+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy