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 1b619a2

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

File tree

14 files changed

+568
-1
lines changed
Filter options

14 files changed

+568
-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",
+23Lines 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+
}
+23Lines 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+
}
+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\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+
}
+74Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
final class MapRequestContentValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface
24+
{
25+
public function __construct(
26+
private readonly SerializerInterface $serializer,
27+
private readonly ?ValidatorInterface $validator,
28+
) {
29+
}
30+
31+
/**
32+
* @deprecated since Symfony 6.2, use resolve() instead
33+
*/
34+
public function supports(Request $request, ArgumentMetadata $argument): bool
35+
{
36+
@trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__);
37+
38+
return 1 === \count($argument->getAttributes(MapRequestContent::class, ArgumentMetadata::IS_INSTANCEOF));
39+
}
40+
41+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
42+
{
43+
$attributes = $argument->getAttributes(MapRequestContent::class, ArgumentMetadata::IS_INSTANCEOF);
44+
45+
if (!$attributes) {
46+
return [];
47+
}
48+
49+
/** @var MapRequestContent $attribute */
50+
$attribute = $attributes[0];
51+
52+
$type = $argument->getType();
53+
if (!$type) {
54+
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->getName()));
55+
}
56+
57+
$payload = $this->serializer->deserialize(
58+
$request->getContent(),
59+
$type,
60+
$attribute->format,
61+
$attribute->context
62+
);
63+
64+
if ($this->validator) {
65+
$violations = $this->validator->validate($payload);
66+
67+
if (\count($violations)) {
68+
throw new ValidationFailedException($payload, $violations);
69+
}
70+
}
71+
72+
return [$payload];
73+
}
74+
}

0 commit comments

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