Skip to content

Commit e980a34

Browse files
feature #49134 [HttpKernel] Add #[MapQueryParameter] to map and validate individual query parameters to controller arguments (ruudk, nicolas-grekas)
This PR was merged into the 6.3 branch. Discussion ---------- [HttpKernel] Add `#[MapQueryParameter]` to map and validate individual query parameters to controller arguments | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? |no | New feature? | yes | Deprecations? |no | Tickets | | License | MIT | Doc PR | 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 #[Route(path: '/', name: 'admin_dashboard')] 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 #[Route(path: '/', name: 'admin_dashboard')] 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. It's possible to use `FILTER_VALIDATE_*` consts for more precise type descriptions. For example, this ensures that `$ids` is an array of integers: ```php #[MapQueryParameter(filter: \FILTER_VALIDATE_INT)] array $ids ``` And this declares a string that must match a regexp: ```php #[MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/^\w++$/'])] string $name ``` When a filter fails, a 404 is returned. Commits ------- bd7c669c89 Register QueryParameterValueResolver as "controller.targeted_value_resolver" 17a8f9288f [HttpKernel] Allow injecting query parameters in controllers by typing them with `#[MapQueryParameter]` attribute
2 parents b4f3d65 + 3efc0e7 commit e980a34

File tree

2 files changed

+5
-0
lines changed

2 files changed

+5
-0
lines changed

DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class UnusedTagsPass implements CompilerPassInterface
4545
'container.service_subscriber',
4646
'container.stack',
4747
'controller.argument_value_resolver',
48+
'controller.targeted_value_resolver',
4849
'controller.service_arguments',
4950
'controller.targeted_value_resolver',
5051
'data_collector',

Resources/config/web.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
1717
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
1818
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
19+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver;
1920
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
2021
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver;
2122
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
@@ -91,6 +92,9 @@
9192
->set('argument_resolver.variadic', VariadicValueResolver::class)
9293
->tag('controller.argument_value_resolver', ['priority' => -150, 'name' => VariadicValueResolver::class])
9394

95+
->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class)
96+
->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class])
97+
9498
->set('response_listener', ResponseListener::class)
9599
->args([
96100
param('kernel.charset'),

0 commit comments

Comments
 (0)