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 427874e

Browse filesBrowse files
committed
Allow injecting query and request 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 2 new attributes: `#[QueryParameter]` and `#[RequestParameter]` 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 #[Route(path: '/', name: 'admin_dashboard')] public function indexAction( #[QueryParameter] array $ids, #[QueryParameter] string $firstName, #[QueryParameter] bool $required, #[QueryParameter] int $age, #[QueryParameter] string $category = '', #[QueryParameter] ?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 #[Route(path: '/', name: 'admin_dashboard')] public function indexAction( #[QueryParameter] 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 f06554b commit 427874e
Copy full SHA for 427874e

File tree

5 files changed

+461
-0
lines changed
Filter options

5 files changed

+461
-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+
* #[QueryParameter] ?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+
* #[QueryParameter] 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 QueryParameter
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+
}
+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 request parameter (POST).
16+
*
17+
* public function homeAction(
18+
* #[RequestParameter] ?string $name
19+
* );
20+
*
21+
* When submitting a POST request with name=John to /home
22+
* It will fill the $name parameter with the value of the request parameter "name".
23+
*
24+
* When submitting a POST request with no values to /home
25+
* It will set $name to null.
26+
*
27+
* When the parameter is non-nullable / not optional:
28+
* public function homeAction(
29+
* #[RequestParameter] string $name
30+
* );
31+
*
32+
* When submitting a POST request with no values to /home it will throw a Bad Request exception.
33+
*/
34+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
35+
final class RequestParameter
36+
{
37+
/**
38+
* @param string|null $name The name of the request 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
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add `#[WithHttpStatus]` for defining status codes for exceptions
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
12+
* Add QueryOrRequestParameterValueResolver that handles `#[QueryParameter]` or `#[RequestParameter]`
1213

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

0 commit comments

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