Skip to content

Commit 89f420f

Browse files
committed
Adding a new Attribute MapRequestHeader class and resolver
1 parent 8d5be07 commit 89f420f

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
2121
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver;
2222
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
23+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver;
2324
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
2425
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
2526
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver;
@@ -100,6 +101,9 @@
100101
->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class)
101102
->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class])
102103

104+
->set('argument_resolver.header_value_resolver', RequestHeaderValueResolver::class)
105+
->tag('controller.targeted_value_resolver', ['name' => RequestHeaderValueResolver::class])
106+
103107
->set('response_listener', ResponseListener::class)
104108
->args([
105109
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+
public readonly int $validationFailedStatusCode = Response::HTTP_BAD_REQUEST,
23+
string $resolver = RequestHeaderValueResolver::class,
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
@@ -19,6 +19,7 @@ CHANGELOG
1919
* Add argument `$buildDir` to `WarmableInterface`
2020
* Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()`
2121
* Add `ControllerResolver::allowControllers()` to define which callables are legit controllers when the `_check_controller_is_allowed` request attribute is set
22+
* Add `#[MapRequestHeader]` to map header from `Request::$headers`
2223

2324
6.3
2425
---
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
$value = null;
37+
38+
if ($request->headers->has($name)) {
39+
$value = match ($type) {
40+
'string' => $request->headers->get($name),
41+
'array' => match (strtolower($name)) {
42+
'accept' => $request->getAcceptableContentTypes(),
43+
'accept-charset' => $request->getCharsets(),
44+
'accept-language' => $request->getLanguages(),
45+
'accept-encoding' => $request->getEncodings(),
46+
default => [$request->headers->get($name)],
47+
},
48+
default => AcceptHeader::fromString($request->headers->get($name)),
49+
};
50+
}
51+
52+
if (null === $value && $argument->hasDefaultValue()) {
53+
$value = $argument->getDefaultValue();
54+
}
55+
56+
if (null === $value && !$argument->isNullable()) {
57+
throw new HttpException($attribute->validationFailedStatusCode, \sprintf('Missing header "%s".', $name));
58+
}
59+
60+
return [$value];
61+
}
62+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 provideHeaderValueWithStringType(): 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 provideHeaderValueWithArrayType(): 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 provideHeaderValueWithAcceptHeaderType(): 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 provideHeaderValueWithDefaultAndNull(): iterable
97+
{
98+
yield 'with hasDefaultValue' => [true, 'foo', false, 'foo'];
99+
yield 'with no isNullable' => [false, null, true, null];
100+
}
101+
102+
public function testWrongType()
103+
{
104+
self::expectException(\LogicException::class);
105+
106+
$metadata = new ArgumentMetadata('accept', 'int', false, false, null, false, [
107+
MapRequestHeader::class => new MapRequestHeader(),
108+
]);
109+
110+
$request = Request::create('/');
111+
112+
$resolver = new RequestHeaderValueResolver();
113+
$resolver->resolve($request, $metadata);
114+
}
115+
116+
/**
117+
* @dataProvider provideHeaderValueWithStringType
118+
*/
119+
public function testWithStringType(string $parameter, string $value)
120+
{
121+
$resolver = new RequestHeaderValueResolver();
122+
123+
$metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [
124+
MapRequestHeader::class => new MapRequestHeader($parameter),
125+
]);
126+
127+
$request = Request::create('/');
128+
$request->headers->set($parameter, $value);
129+
130+
$arguments = $resolver->resolve($request, $metadata);
131+
132+
self::assertEquals([$value], $arguments);
133+
}
134+
135+
/**
136+
* @dataProvider provideHeaderValueWithArrayType
137+
*/
138+
public function testWithArrayType(string $parameter, string $value, array $expected)
139+
{
140+
$resolver = new RequestHeaderValueResolver();
141+
142+
$metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [
143+
MapRequestHeader::class => new MapRequestHeader($parameter),
144+
]);
145+
146+
$request = Request::create('/');
147+
$request->headers->set($parameter, $value);
148+
149+
$arguments = $resolver->resolve($request, $metadata);
150+
151+
self::assertEquals($expected, $arguments);
152+
}
153+
154+
/**
155+
* @dataProvider provideHeaderValueWithAcceptHeaderType
156+
*/
157+
public function testWithAcceptHeaderType(string $parameter, string $value, array $expected)
158+
{
159+
$resolver = new RequestHeaderValueResolver();
160+
161+
$metadata = new ArgumentMetadata('variableName', AcceptHeader::class, false, false, null, false, [
162+
MapRequestHeader::class => new MapRequestHeader($parameter),
163+
]);
164+
165+
$request = Request::create('/');
166+
$request->headers->set($parameter, $value);
167+
168+
$arguments = $resolver->resolve($request, $metadata);
169+
170+
self::assertEquals($expected, $arguments);
171+
}
172+
173+
/**
174+
* @dataProvider provideHeaderValueWithDefaultAndNull
175+
*/
176+
public function testWithDefaultValueAndNull(bool $hasDefaultValue, ?string $defaultValue, bool $isNullable, ?string $expected)
177+
{
178+
$metadata = new ArgumentMetadata('wrong-header', 'string', false, $hasDefaultValue, $defaultValue, $isNullable, [
179+
MapRequestHeader::class => new MapRequestHeader(),
180+
]);
181+
182+
$request = Request::create('/');
183+
184+
$resolver = new RequestHeaderValueResolver();
185+
$arguments = $resolver->resolve($request, $metadata);
186+
187+
self::assertEquals([$expected], $arguments);
188+
}
189+
190+
public function testWithNoDefaultAndNotNullable()
191+
{
192+
self::expectException(HttpException::class);
193+
self::expectExceptionMessage('Missing header "variableName".');
194+
195+
$metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [
196+
MapRequestHeader::class => new MapRequestHeader(),
197+
]);
198+
199+
$resolver = new RequestHeaderValueResolver();
200+
$resolver->resolve(Request::create('/'), $metadata);
201+
}
202+
}

0 commit comments

Comments
 (0)