8000 Adding a new Attribute MapRequestHeader class and resolver · symfony/symfony@7494a85 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7494a85

Browse files
committed
Adding a new Attribute MapRequestHeader class and resolver
1 parent 8b420d4 commit 7494a85

File tree

5 files changed

+279
-0
lines changed

5 files changed

+279
-0
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
1919
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver;
2020
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
21+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver;
2122
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
2223
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
2324
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver;
@@ -97,6 +98,13 @@
9798
->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class)
9899
->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class])
99100

101+
->set('argument_resolver.header_value_resolver', RequestHeaderValueResolver::class)
102+
->args([
103+
service('serializer'),
104+
service('validator')->nullOnInvalid(),
105+
])
106+
->tag('controller.targeted_value_resolver', ['name' => RequestHeaderValueResolver::class])
107+
100108
->set('response_listener', ResponseListener::class)
101109
->args([
102110
param('kernel.charset'),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver;
16+
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapRequestHeader extends ValueResolver
19+
{
20+
public function __construct(
21+
public readonly ?string $name = null,
22+
string $resolver = RequestHeaderValueResolver::class,
23+
public readonly int $validationFailedStatusCode = Response::HTTP_BAD_REQUEST,
24+
) {
25+
parent::__construct($resolver);
26+
}
27+
}

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CHANGELOG
1414
* Deprecate `Kernel::stripComments()`
1515
* Support the `!` character at the beginning of a string as a negation operator in the url filter of the profiler
1616
* Deprecate `UriSigner`, use `UriSigner` from the HttpFoundation component instead
17+
* Add `#[MapRequestHeader]` and `#[MapRequestHeaders]` to map and validate request header from `Request::$headers`
1718

1819
6.3
1920
---
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\AcceptHeader;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;
17+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
use Symfony\Component\HttpKernel\Exception\HttpException;
20+
21+
class RequestHeaderValueResolver implements ValueResolverInterface
22+
{
23+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
24+
{
25+
if (!$attribute = $argument->getAttributesOfType(MapRequestHeader::class)[0] ?? null) {
26+
return [];
27+
}
28+
29+
$type = $argument->getType();
30+
31+
if (!\in_array($type, ['string', 'array', AcceptHeader::class])) {
32+
throw new \LogicException(\sprintf('Could not resolve the argument typed "%s". Valid values types are "array", "string" or "%s".', $type, AcceptHeader::class));
33+
}
34+
35+
$name = $attribute->name ?? $argument->getName();
36+
37+
$value = match ($type) {
38+
'string' => $request->headers->get($name),
39+
'array' => match (strtolower($name)) {
40+
'accept' => $request->getAcceptableContentTypes(),
41+
'accept-charset' => $request->getCharsets(),
42+
'accept-language' => $request->getLanguages(),
43+
'accept-encoding' => $request->getEncodings(),
44+
default => $request->headers->get($name) ? [$request->headers->get($name)] : [],
45+
},
46+
default => AcceptHeader::fromString($request->headers->get($name)),
47+
};
48+
49+
if (null === $value && !$argument->isNullable()) {
50+
throw new HttpException($attribute->validationFailedStatusCode, \sprintf('Missing header "%s".', $name));
51+
}
52+
53+
return [$value];
54+
}
55+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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\Tests\Controller\ArgumentResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\AcceptHeader;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;
18+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver;
19+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
20+
use Symfony\Component\HttpKernel\Exception\HttpException;
21+
22+
class RequestHeaderValueResolverTest extends TestCase
23+
{
24+
public static function provideHeaderValuesWithStringType(): iterable
25+
{
26+
yield 'with accept' => ['accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'];
27+
yield 'with accept-language' => ['accept-language', 'en-us,en;q=0.5'];
28+
yield 'with host' => ['host', 'localhost'];
29+
yield 'with user-agent' => ['user-agent', 'Symfony'];
30+
}
31+
32+
public static function provideHeaderValuesWithArrayType(): iterable
33+
{
34+
yield 'with accept' => [
35+
'accept',
36+
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
37+
[
38+
[
39+
'text/html',
40+
'application/xhtml+xml',
41+
'application/xml',
42+
'*/*',
43+
],
44+
],
45+
];
46+
yield 'with accept-language' => [
47+
'accept-language',
48+
'en-us,en;q=0.5',
49+
[
50+
[
51+
'en_US',
52+
'en',
53+
],
54+
],
55+
];
56+
yield 'with host' => [
57+
'host',
58+
'localhost',
59+
[
60+
['localhost'],
61+
],
62+
];
63+
yield 'with user-agent' => [
64+
'user-agent',
65+
'Symfony',
66+
[
67+
['Symfony'],
68+
],
69+
];
70+
}
71+
72+
public static function provideHeaderValuesWithAcceptHeaderType(): iterable
73+
{
74+
yield 'with accept' => [
75+
'accept',
76+
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
77+
[AcceptHeader::fromString('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')],
78+
];
79+
yield 'with accept-language' => [
80+
'accept-language',
81+
'en-us,en;q=0.5',
82+
[AcceptHeader::fromString('en-us,en;q=0.5')],
83+
];
84+
yield 'with host' => [
85+
'host',
86+
'localhost',
87+
[AcceptHeader::fromString('localhost')],
88+
];
89+
yield 'with user-agent' => [
90+
'user-agent',
91+
'Symfony',
92+
[AcceptHeader::fromString('Symfony')],
93+
];
94+
}
95+
96+
public static function provideNoValueNullable(): iterable
97+
{
98+
yield 'with array type' => ['array', [[]]];
99+
yield 'with string type' => ['string', [null]];
100+
}
101+
102+
/**
103+
* @dataProvider provideHeaderValuesWithStringType
104+
*/
105+
public function testWithStringType(string $parameter, string $value)
106+
{
107+
$resolver = new RequestHeaderValueResolver();
108+
109+
$metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [
110+
MapRequestHeader::class => new MapRequestHeader($parameter),
111+
]);
112+
113+
$request = Request::create('/');
114+
$request->headers->set($parameter, $value);
115+
116+
$arguments = $resolver->resolve($request, $metadata);
117+
118+
self::assertEquals([$value], $arguments);
119+
}
120+
121+
/**
122+
* @dataProvider provideHeaderValuesWithArrayType
123+
*/
124+
public function testWithArrayType(string $parameter, string $value, array $expected)
125+
{
126+
$resolver = new RequestHeaderValueResolver();
127+
128+
$metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [
129+
MapRequestHeader::class => new MapRequestHeader($parameter),
130+
]);
131+
132+
$request = Request::create('/');
133+
$request->headers->set($parameter, $value);
134+
135+
$arguments = $resolver->resolve($request, $metadata);
136+
137+
self::assertEquals($expected, $arguments);
138+
}
139+
140+
/**
141+
* @dataProvider provideHeaderValuesWithAcceptHeaderType
142+
*/
143+
public function testWithAcceptHeaderType(string $parameter, string $value, array $expected)
144+
{
145+
$resolver = new RequestHeaderValueResolver();
146+
147+
$metadata = new ArgumentMetadata('variableName', AcceptHeader::class, false, false, null, false, [
148+
MapRequestHeader::class => new MapRequestHeader($parameter),
149+
]);
150+
151+
$request = Request::create('/');
152+
$request->headers->set($parameter, $value);
153+
154+
$arguments = $resolver->resolve($request, $metadata);
155+
156+
self::assertEquals($expected, $arguments);
157+
}
158+
159+
/**
160+
* @dataProvider provideNoValueNullable
161+
*/
162+
public function testWithNoValueNullable(string $type, array $expected)
163+
{
164+
$resolver = new RequestHeaderValueResolver();
165+
166+
$metadata = new ArgumentMetadata('variableName', $type, false, false, null, true, [
167+
MapRequestHeader::class => new MapRequestHeader(),
168+
]);
169+
170+
$arguments = $resolver->resolve(Request::create('/'), $metadata);
171+
172+
self::assertEquals($expected, $arguments);
173+
}
174+
175+
public function testWithNoStringValueNotNullable()
176+
{
177+
$resolver = new RequestHeaderValueResolver();
178+
179+
self::expectException(HttpException::class);
180+
self::expectExceptionMessage('Missing header "variableName".');
181+
182+
$metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [
183+
MapRequestHeader::class => new MapRequestHeader(),
184+
]);
185+
186+
$resolver->resolve(Request::create('/'), $metadata);
187+
}
188+
}

0 commit comments

Comments
 (0)
0