Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 161d283

Browse filesBrowse files
committed
Allow injecting query parameters in controllers
We increased the PHPStan level from 8 to 9. This lead to some problems when working with query or request parameters. For example: ```php $firstName = $request->get('firstName'); ``` Because Symfony types `Request::get()` as `mixed` there is no type safety and you have to assert everything manually. We then learned that we shouldn't use `Request::get()` but use the explicit parameter bag like: ```php $request->query->get('firstName'); ``` This `ParameterBag` is used for `request` and `query`. It contains interesting methods like: ```php $request->query->getAlpha('firstName') : string; $request->query->getInt('age') : int; ``` This has the benefit that now the returned value is of a correct type. Why aren't we being explicit by requiring the parameters and their types in the controller action instead? Luckily Symfony has a concept called [ValueResolver](https://symfony.com/doc/current/controller/value_resolver.html). It allows you to do dynamically alter what is injected into a controller action. So in this PR, we introduces a new attribute: `#[MapQueryParameter]` that can be used in controller arguments. It allows you to define which parameters your controller is using and which type they should be. For example: ```php public function indexAction( #[MapQueryParameter] array $ids, #[MapQueryParameter] string $firstName, #[MapQueryParameter] bool $required, #[MapQueryParameter] int $age, #[MapQueryParameter] string $category = '', #[MapQueryParameter] ?string $theme = null, ) ``` When requesting `/?ids[]=1&ids[]=2&firstName=Ruud&required=3&age=123` you'll get: ``` $ids = ['1', '2'] $firstName = "Ruud" $required = false $age = 123 $category = '' $theme = null ``` It even supports variadic arguments like this: ```php public function indexAction( #[MapQueryParameter] string ...$ids, ) ``` When requesting `/?ids[]=111&ids[]=222` the `$ids` argument will have an array with values ['111','222']. Unit testing the controller now also becomes a bit easier, as you only have to pass the required parameters instead of constructing the `Request` object.
1 parent f3722d5 commit 161d283
Copy full SHA for 161d283

File tree

4 files changed

+403
-0
lines changed
Filter options

4 files changed

+403
-0
lines changed
+44Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
/**
15+
* Can be used in a controller action to mark a parameter as a query parameter.
16+
*
17+
* public function homeAction(
18+
* #[MapQueryParameter] ?string $name
19+
* );
20+
*
21+
* When opening this URL: /home?name=John
22+
* It will fill the $name parameter with the value of the query parameter "name".
23+
*
24+
* When opening this URL: /home
25+
* It will set $name to null.
26+
*
27+
* When the parameter is non-nullable / not optional:
28+
* public function homeAction(
29+
* #[MapQueryParameter] string $name
30+
* );
31+
*
32+
* Opening /home without passing ?name=John will throw a Bad Request exception.
33+
*/
34+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
35+
final class MapQueryParameter
36+
{
37+
/**
38+
* @param string|null $name The name of the query parameter. If null, the name of the parameter in the action will be used.
39+
*/
40+
public function __construct(
41+
public ?string $name = null,
42+
) {
43+
}
44+
}

‎src/Symfony/Component/HttpKernel/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
* Use an instance of `Psr\Clock\ClockInterface` to generate the current date time in `DateTimeValueResolver`
1111
* Add `#[WithLogLevel]` for defining log levels for exceptions
1212
* Add `skip_response_headers` to the `HttpCache` options
13+
* Add QueryParameterValueResolver that handles `#[MapQueryParameter]`
1314

1415
6.2
1516
---
+77Lines changed: 77 additions & 0 deletions
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 <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\Exception\BadRequestException;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
17+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
20+
final class MapQueryParameterValueResolver implements ValueResolverInterface
21+
{
22+
/**
23+
* @return array<mixed>
24+
*/
25+
public function resolve(Request $request, ArgumentMetadata $argument): array
26+
{
27+
$attribute = $argument->getAttributesOfType(MapQueryParameter::class)[0] ?? null;
28+
if (null === $attribute) {
29+
return [];
30+
}
31+
32+
$name = $attribute->name ?? $argument->getName();
33+
if (false === $request->query->has($name)) {
34+
if ($argument->isNullable() || $argument->hasDefaultValue()) {
35+
return [];
36+
}
37+
38+
throw new BadRequestException(sprintf('Missing query parameter "%s".', $name));
39+
}
40+
41+
if ($argument->isVariadic()) {
42+
return $request->query->all($name);
43+
}
44+
45+
if ('array' === $argument->getType()) {
46+
return [$request->query->all($name)];
47+
}
48+
49+
$value = $request->query->get($name);
50+
51+
if ('string' === $argument->getType()) {
52+
return [$value];
53+
}
54+
55+
if ('int' === $argument->getType()) {
56+
if (!is_numeric($value)) {
57+
throw new BadRequestException(sprintf('Query parameter "%s" must be an integer.', $name));
58+
}
59+
60+
return [(int) $value];
61+
}
62+
63+
if ('float' === $argument->getType()) {
64+
if (!is_numeric($value)) {
65+
throw new BadRequestException(sprintf('Query parameter "%s" must be a float.', $name));
66+
}
67+
68+
return [(float) $value];
69+
}
70+
71+
if ('bool' === $argument->getType()) {
72+
return [filter_var($value, \FILTER_VALIDATE_BOOLEAN)];
73+
}
74+
75+
return [];
76+
}
77+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.