Skip to content

[HttpKernel] Replace ArgumentValueResolverInterface by ValueResolverInterface #17362

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

Merged
merged 1 commit into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _build/redirection_map
Original file line number Diff line number Diff line change
Expand Up @@ -538,3 +538,4 @@
/email /mailer
/frontend/assetic /frontend
/frontend/assetic/index /frontend
/controller/argument_value_resolver /controller/value_resolver
167 changes: 54 additions & 113 deletions controller/argument_value_resolver.rst → controller/value_resolver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In the :doc:`controller guide </controller>`, you've learned that you can get th
your controller. This argument has to be type-hinted by the ``Request`` class
in order to be recognized. This is done via the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`. By
creating and registering custom argument value resolvers, you can extend this
creating and registering custom value resolvers, you can extend this
functionality.

.. _functionality-shipped-with-the-httpkernel:
Expand Down Expand Up @@ -168,132 +168,88 @@ PSR-7 Objects Resolver:
Adding a Custom Value Resolver
------------------------------

In the next example, you'll create a value resolver to inject the object that
represents the current user whenever a controller method type-hints an argument
with the ``User`` class::
In the next example, you'll create a value resolver to inject an ID value
object whenever a controller argument has a type implementing
``IdentifierInterface`` (e.g. ``BookingId``)::

// src/Controller/UserController.php
// src/Controller/BookingController.php
namespace App\Controller;

use App\Entity\User;
use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;

class UserController
class BookingController
{
public function index(User $user)
public function index(BookingId $id): Response
{
return new Response('Hello '.$user->getUserIdentifier().'!');
// ... do something with $id
}
}

Beware that this feature is already provided by the `#[ParamConverter]`_
attribute from the SensioFrameworkExtraBundle. If you have that bundle
installed in your project, add this config to disable the auto-conversion of
type-hinted method arguments:
.. versionadded:: 6.2

.. configuration-block::

.. code-block:: yaml

# config/packages/sensio_framework_extra.yaml
sensio_framework_extra:
request:
converters: true
auto_convert: false

.. code-block:: xml

<!-- config/packages/sensio_framework_extra.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sensio-framework-extra="http://symfony.com/schema/dic/symfony_extra"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony_extra
https://symfony.com/schema/dic/symfony_extra/symfony_extra-1.0.xsd">

<sensio-framework-extra:config>
<request converters="true" auto-convert="false"/>
</sensio-framework-extra:config>
</container>

.. code-block:: php

// config/packages/sensio_framework_extra.php
$container->loadFromExtension('sensio_framework_extra', [
'request' => [
'converters' => true,
'auto_convert' => false,
],
]);
The ``ValueResolverInterface`` was introduced in Symfony 6.2. Prior to
6.2, you had to use the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`,
which defines different methods.

Adding a new value resolver requires creating a class that implements
:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`
and defining a service for it. The interface defines two methods:

``supports()``
This method is used to check whether the value resolver supports the
given argument. ``resolve()`` will only be called when this returns ``true``.
``resolve()``
This method will resolve the actual value for the argument. Once the value
is resolved, you must `yield`_ the value to the ``ArgumentResolver``.
:class:`Symfony\\Component\\HttpKernel\\Controller\\ValueResolverInterface`
and defining a service for it.

Both methods get the ``Request`` object, which is the current request, and an
This interface contains a ``resolve()`` method, which is called for each
argument of the controller. It receives the current ``Request`` object and an
:class:`Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata`
instance. This object contains all information retrieved from the method signature
for the current argument.
instance, which contains all information from the method signature. The
method should return either an empty array (if it cannot resolve this
argument) or an array with the resolved value(s).

Now that you know what to do, you can implement this interface. To get the
current ``User``, you need the current security token. This token can be
retrieved from the token storage::
.. code-block:: php

// src/ArgumentResolver/UserValueResolver.php
namespace App\ArgumentResolver;
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security\Security;
use App\IdentifierInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

class UserValueResolver implements ArgumentValueResolverInterface
class UserValueResolver implements ValueResolverInterface
{
private $security;

public function __construct(Security $security)
public function resolve(Request $request, ArgumentMetadata $argument): array
{
$this->security = $security;
}

public function supports(Request $request, ArgumentMetadata $argument): bool
{
if (User::class !== $argument->getType()) {
return false;
// get the argument type (e.g. BookingId)
$argumentType = $argument->getType();
if (
!$argumentType
|| !is_subclass_of($argumentType, IdentifierInterface::class, true)
) {
return [];
}

return $this->security->getUser() instanceof User;
}
// get the value from the request, based on the argument name
$value = $request->attributes->get($argument->getName());
if (!is_string($value)) {
return [];
}

public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
yield $this->security->getUser();
// create and return the value object
return [$argumentType::fromString($value)];
}
}

In order to get the actual ``User`` object in your argument, the given value
must fulfill the following requirements:
This method first checks whether it can resolve the value:

* An argument must be type-hinted as ``User`` in your action method signature;
* The value must be an instance of the ``User`` class.
* The argument must be type-hinted with a class implementing a custom ``IdentifierInterface``;
* The argument name (e.g. ``$id``) must match the name of a request
attribute (e.g. using a ``/booking/{id}`` route placeholder).

When all those requirements are met and ``true`` is returned, the
``ArgumentResolver`` calls ``resolve()`` with the same values as it called
``supports()``.
When those requirements are met, the method creates a new instance of the
custom value object and returns it as the value for this argument.

That's it! Now all you have to do is add the configuration for the service
container. This can be done by tagging the service with ``controller.argument_value_resolver``
and adding a priority.
and adding a priority:

.. configuration-block::

Expand All @@ -308,7 +264,7 @@ and adding a priority.

App\ArgumentResolver\UserValueResolver:
tags:
- { name: controller.argument_value_resolver, priority: 50 }
- { name: controller.argument_value_resolver, priority: 150 }

.. code-block:: xml

Expand All @@ -325,7 +281,7 @@ and adding a priority.
<!-- ... -->

<service id="App\ArgumentResolver\UserValueResolver">
<tag name="controller.argument_value_resolver" priority="50"/>
<tag name="controller.argument_value_resolver" priority="150"/>
</service>
</services>

Expand All @@ -342,7 +298,7 @@ and adding a priority.
$services = $configurator->services();

$services->set(UserValueResolver::class)
->tag('controller.argument_value_resolver', ['priority' => 50])
->tag('controller.argument_value_resolver', ['priority' => 150])
;
};

Expand All @@ -351,26 +307,11 @@ the expected value is injected. The built-in ``RequestAttributeValueResolver``,
which fetches attributes from the ``Request``, has a priority of ``100``. If your
resolver also fetches ``Request`` attributes, set a priority of ``100`` or more.
Otherwise, set a priority lower than ``100`` to make sure the argument resolver
is not triggered when the ``Request`` attribute is present (for example, when
passing the user along sub-requests).
is not triggered when the ``Request`` attribute is present.

To ensure your resolvers are added in the right position you can run the following
command to see which argument resolvers are present and in which order they run.
command to see which argument resolvers are present and in which order they run:

.. code-block:: terminal

$ php bin/console debug:container debug.argument_resolver.inner --show-arguments

.. tip::

As you can see in the ``UserValueResolver::supports()`` method, the user
may not be available (e.g. when the controller is not behind a firewall).
In these cases, the resolver will not be executed. If no argument value
is resolved, an exception will be thrown.

To prevent this, you can add a default value in the controller (e.g. ``User
$user = null``). The ``DefaultValueResolver`` is executed as the last
resolver and will use the default value if no value was already resolved.

.. _`#[ParamConverter]`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
.. _`yield`: https://www.php.net/manual/en/language.generators.syntax.php