Skip to content

Commit 161d283

Browse 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

File tree

4 files changed

+403
-0
lines changed

4 files changed

+403
-0
lines changed
Lines 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

Lines 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
---
Lines 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)