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

Skip to content

Commit a08be6b

Browse files
author
Konstantin Myakshin
committed
Create Attributes to map Query String and Request Content to typed objects
1 parent f06554b commit a08be6b

File tree

11 files changed

+434
-0
lines changed

11 files changed

+434
-0
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
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+
yield $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+
yield $payload;
76+
}
77+
}

0 commit comments

Comments
 (0)