-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[HttpKernel] Adding new #[MapRequestHeader]
attribute and resolver
#51379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.4
Are you sure you want to change the base?
[HttpKernel] Adding new #[MapRequestHeader]
attribute and resolver
#51379
Conversation
Hey! I see that this is your first PR. That is great! Welcome! Symfony has a contribution guide which I suggest you to read. In short:
Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change. When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor! I am going to sit back now and wait for the reviews. Cheers! Carsonbot |
527c461
to
e85136d
Compare
Maybe I'm wrong but seems that something related to symfony/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php Line 84 in 16a4681
Why did not add in the same file? |
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Show resolved
Hide resolved
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this not be best to also allow it to map to an object, like how the MapRequestPayload
currently works, as I can pass an Object
as a reference the serializer will deserialize to this object? That way you can have a Header
class that the headers deserialize into.
You can merge it with your current concept and have a bit of best of both? Map individual Headers to an string, or an object or even just an array if required?
Something potentially like this?
private function mapRequestHeader(Request $request, string $type, MapRequestHeader $attribute): ?object
{
$headers = [];
$requestHeaders = $request->headers->all();
array_walk($requestHeaders, function ($value, $key) use (&$headers) {
$key = $this->snakeCaseToCamelCase($key);
$headers[$key] = $value[0];
});
$headers = json_encode($headers);
$format = 'json';
try {
return $this->serializer->deserialize($headers, $type, $format, []);
} catch (UnsupportedFormatException $e) {
throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format: "%s".', $format), $e);
} catch (NotEncodableValueException $e) {
throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains invalid "%s" data.', $format), $e);
}
}
and you could have a class like this:
class HeaderRequest
{
public function __construct(
public readonly string $userAgent,
public readonly string $accept,
public readonly string $contentType,
public readonly string $acceptEncoding,
) {
}
}
An advantage is you can use the Validator too and enforce any header types if needed or create a HeaderRequest class per request as needed.
You can add this as a new method to the existing RequestPayloadValueResolver
as already mentioned by @AurelienPillevesse so it will tie nicely into the existing process.
This is a rough example but just food for thought.
Very useful PR this and something we need, so looking forward to seeing how it progresses :)
261f6e6
to
72cb1ee
Compare
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
Outdated
Show resolved
Hide resolved
afbdd35
to
90f1015
Compare
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
Outdated
Show resolved
Hide resolved
Too bad the PR was changed after @robert-sykes suggestion. I think these are 2 distinct ideas. The original PR description mentions a single attribute per header, same as how MapQueryParameter works. There are valid cases for that, where you only want and need a single header scalar. The updated code now requires you to map a header to a header object. Would be great to also keep supporting the original one for just a single header. See this comment where a similar situation happened: #49134 (comment) |
I'm all for this, to be fair. I did imply this in my original comment where I said @StevenRenaux You could check the type and if it's a string map to that similar to your original commit, otherwise if it's object map to those fields instead. We get best of both worlds this way. What do you think? |
Hey, I will try to do it and I didn't understand well for sure but we are there to share and go further 😉 |
No problem at all. If you need a hand let me know, more than happy to pair on this with you. :) |
ff6b41f
to
0bda0bf
Compare
To get back to the original Issue I suggest this namespace App\Controller;
class TestController extends AbstractController
{
#[Route(path: '/', name: 'test')]
public function test(
#[MapRequestHeader] HeaderRequestDto $headerRequestDto,
#[MapRequestHeader(name: 'user-agent')] string $agent,
#[MapRequestHeader] array $accept
): Response
{
dd(
$headerRequestDto,
// App\Entity\HeaderRequestDto {
// accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
// agent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
// acceptEncoding: "gzip, deflate, br"
// host: "localhost:8000"
// }
$agent,
// "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/116.0"
$accept
// array:6 [
// 0 => "text/html"
// 1 => "application/xhtml+xml"
// 2 => "application/xml;q=0.9"
// 3 => "image/avif"
// 4 => "image/webp"
// 5 => "*/*;q=0.8"
// ]
);
}
} And a Dto could look like this : namespace App\Entity;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
class HeaderRequestDto
{
public function __construct(
public readonly string $accept,
#[Assert\EqualTo('test', groups: ['test'])]
#[SerializedName('user-agent')] public readonly string $agent = 'agent',
#[SerializedName('accept-encoding')] public readonly string $acceptEncoding,
#[Assert\EqualTo('localhost', groups: ['valid'])]
public readonly string $host
) {
}
} We only use kebab-case (as returned by HeaderBag) now to get some property like 'user-agent'. |
0bda0bf
to
cced8c1
Compare
Hi @StevenRenaux! What's the current state of this PR? 🙂 |
@alexandre-daubois Waiting for review, since the last requested update. |
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php
Outdated
Show resolved
Hide resolved
#[MapRequestHeader]
and #[MapRequestHeaders]
attributes and resolver#[MapRequestHeader]
attribute and resolver
7494a85
to
0f38cde
Compare
Back to the basics of PR, using only the MapRequestHeader attribute. Mapping to a DTO does not seem like a relevant or common use case or if it is really necessary could be done in a future PR. The allowed types are therefore:
When the header parameter cannot be determined by the variable name, we can specify the name argument in the attribute. public function test(
#[MapRequestHeader('user-agent')] ?string $bar,
): Response
{
dump($bar);
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
} If we are looking for a header of type Accept-* with an array type, we will use the dedicated methods to retrieve this data. https://symfony.com/doc/current/components/http_foundation.html#accessing-accept-headers-data public function test(
#[MapRequestHeader] array $accept,
): Response
{
dump($accept);
// array:7 [▼
// 0 => "text/html"
// 1 => "application/xhtml+xml"
// 2 => "image/avif"
// 3 => "image/webp"
// 4 => "image/apng"
// 5 => "application/xml"
// 6 => "*/*"
// ]
} And with AcceptHeader type public function test(
#[MapRequestHeader(name: 'accept-encoding')] AcceptHeader $encoding,
): Response
{
dump($encoding);
// Symfony\Component\HttpFoundation\AcceptHeader {#4727 ▼
// -items: array:4 [▼
// "gzip" =>
// Symfony\Component\HttpFoundation\AcceptHeaderItem {#2437 ▼
// -value: "gzip"
// -quality: 1.0
// -index: 0
// -attributes: []
// }
// "deflate" =>
// Symfony\Component\HttpFoundation\AcceptHeaderItem {#4882 ▼
// -value: "deflate"
// -quality: 1.0
// -index: 1
// -attributes: []
// }
// "br" =>
// Symfony\Component\HttpFoundation\AcceptHeaderItem {#4549 ▼
// -value: "br"
// -quality: 1.0
// -index: 2
// -attributes: []
// }
// "zstd" =>
// Symfony\Component\HttpFoundation\AcceptHeaderItem {#4773 ▼
// -value: "zstd"
// -quality: 1.0
// -index: 3
// -attributes: []
// }
// ]
// -sorted: false
// }
} |
fbde828
to
89f420f
Compare
It bugs me the
|
@StevenRenaux What's needed to get this to a mergeable state? I would love this to be included in the new Symfony version. |
@ruudk Waiting reviews from core-contributors, but it's seems to late for merging into the next 7.3 version. |
Could you rebase it, so that the tests go green? |
89f420f
to
2142045
Compare
@StevenRenaux Thanks! 🙏 It needs a few tweaks in https://github.com/symfony/symfony/actions/runs/15253101552/job/42894283925?pr=51379 |
2142045
to
7a123ea
Compare
'accept-charset' => $request->getCharsets(), | ||
'accept-language' => $request->getLanguages(), | ||
'accept-encoding' => $request->getEncodings(), | ||
default => [$request->headers->get($name)], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default => [$request->headers->get($name)], | |
default => $request->headers->all($name), |
} | ||
|
||
if (null === $value && !$argument->isNullable()) { | ||
throw new HttpException($attribute->validationFailedStatusCode, \sprintf('Missing header "%s".', $name)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if type is array we could return an empty array
Hi,
Related to this #51342 I tried to work on this PR.
To use this new attribute, you just need to add it in your method of your controller.
For some parameters as
User-Agent
we need to add the name for the resolver, because the conversion ofuserAgent
is not interpreted by the resolver and return an exception as Missing header parameter "userAgent".And for some others parameters as cookie I don't know what to do, because at the moment it only return string for all of them and is it really useful 🤷