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 b123fa6

Browse filesBrowse files
committed
Create Attributes to map Query String and Request Content to typed objects
1 parent a08a41c commit b123fa6
Copy full SHA for b123fa6

File tree

Expand file treeCollapse file tree

14 files changed

+578
-1
lines changed
Filter options
Expand file treeCollapse file tree

14 files changed

+578
-1
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
+16Lines 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

+86Lines 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+
}
+16Lines 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+
];
+8Lines 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 }
+7Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/composer.json
+1-1Lines 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",
+25Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
* @author Konstantin Myakshin <molodchick@gmail.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
20+
final class MapQueryString
21+
{
22+
public function __construct(public readonly array $context = [])
23+
{
24+
}
25+
}
+25Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
* @author Konstantin Myakshin <molodchick@gmail.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
20+
final class MapRequestContent
21+
{
22+
public function __construct(public readonly string $format = 'json', public readonly array $context = [])
23+
{
24+
}
25+
}
+80Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
/**
25+
* @author Konstantin Myakshin <molodchick@gmail.com>
26+
*/
27+
final class MapQueryStringValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface
28+
{
29+
private const CONTEXT = [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true];
30+
31+
public function __construct(
32+
private readonly DenormalizerInterface $normalizer,
33+
private readonly ?ValidatorInterface $validator,
34+
) {
35+
}
36+
37+
/**
38+
* @deprecated since Symfony 6.2, use resolve() instead
39+
*/
40+
public function supports(Request $request, ArgumentMetadata $argument): bool
41+
{
42+
@trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__);
43+
44+
return 1 === \count($argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF));
45+
}
46+
47+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
48+
{
49+
$attributes = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF);
50+
51+
if (!$attributes) {
52+
return [];
53+
}
54+
55+
/** @var MapQueryString $attribute */
56+
$attribute = $attributes[0];
57+
58+
$type = $argument->getType();
59+
if (!$type) {
60+
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->getName()));
61+
}
62+
63+
$payload = $this->normalizer->denormalize(
64+
$request->query->all(),
65+
$type,
66+
'json',
67+
$attribute->context + self::CONTEXT
68+
);
69+
70+
if ($this->validator) {
71+
$violations = $this->validator->validate($payload);
72+
73+
if (\count($violations)) {
74+
throw new ValidationFailedException($payload, $violations);
75+
}
76+
}
77+
78+
return [$payload];
79+
}
80+
}
+77Lines 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\MapRequestContent;
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\SerializerInterface;
20+
use Symfony\Component\Validator\Exception\ValidationFailedException;
21+
use Symfony\Component\Validator\Validator\ValidatorInterface;
22+
23+
/**
24+
* @author Konstantin Myakshin <molodchick@gmail.com>
25+
*/
26+
final class MapRequestContentValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface
27+
{
28+
public function __construct(
29+
private readonly SerializerInterface $serializer,
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->getAttributesOfType(MapRequestContent::class));
42+
}
43+
44+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
45+
{
46+
$attributes = $argument->getAttributesOfType(MapRequestContent::class);
47+
48+
if (!$attributes) {
49+
return [];
50+
}
51+
52+
/** @var MapRequestContent $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->serializer->deserialize(
61+
$request->getContent(),
62+
$type,
63+
$attribute->format,
64+
$attribute->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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.