Skip to content

Commit 2142045

Browse files
committed
Adding a new Attribute MapRequestHeader class and resolver
1 parent 69018fe commit 2142045

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;
@@ -101,6 +102,9 @@
101102
->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class)
102103
->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class])
103104

105+
->set('argument_resolver.header_value_resolver', RequestHeaderValueResolver::class)
106+
->tag('controller.targeted_value_resolver', ['name' => RequestHeaderValueResolver::class])
107+
104108
->set('response_listener', ResponseListener::class)
105109
->args([
106110
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
@@ -64,6 +64,7 @@ CHANGELOG
6464
* Add argument `$buildDir` to `WarmableInterface`
6565
* Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()`
6666
* Add `ControllerResolver::allowControllers()` to define which callables are legit controllers when the `_check_controller_is_allowed` request attribute is set
67+
* Add `#[MapRequestHeader]` to map header from `Request::$headers`
6768

6869
6.3
6970
---
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy