Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4e8cb8e

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

File tree

14 files changed

+574
-1
lines changed

14 files changed

+574
-1
lines changed

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

+3
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

+16
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

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 <[email protected]>
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+
}
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 <[email protected]>
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+
];
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 }
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

+1-1
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",
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 <[email protected]>
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+
}
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 <[email protected]>
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+
}
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 <[email protected]>
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+
/**
45+
* @return iterable<object>
46+
*/
47+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
48+
{
49+
$attributes = $argument->getAttributes(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+
}
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 <[email protected]>
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+
/**
42+
* @return iterable<object>
43+
*/
44+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
45+
{
46+
$attributes = $argument->getAttributes(MapRequestContent::class, ArgumentMetadata::IS_INSTANCEOF);
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)