Skip to content
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

Documented the ArgumentResolver along the ControllerResolver #6422

Closed
wants to merge 1 commit into from
Closed
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
123 changes: 80 additions & 43 deletions components/http_kernel/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ to the events discussed below::
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// create the Request object
Expand All @@ -98,10 +100,12 @@ to the events discussed below::
$dispatcher = new EventDispatcher();
// ... add some event listeners

// create your controller resolver
$resolver = new ControllerResolver();
// create your controller and argument resolvers
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

// instantiate the kernel
$kernel = new HttpKernel($dispatcher, $resolver);
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

// actually execute the kernel, which turns the request into a response
// by dispatching events, calling a controller, and returning the response
Expand All @@ -118,6 +122,14 @@ See ":ref:`http-kernel-working-example`" for a more concrete implementation.
For general information on adding listeners to the events below, see
:ref:`http-kernel-creating-listener`.


.. caution::

As of 3.1 the :class:`Symfony\\Component\\Httpkernel\\HttpKernel` accepts a fourth argument, which
must be an instance of :class:`Symfony\\Component\\Httpkernel\\Controller\\ArgumentResolverInterface`.
In 4.0 this argument will become mandatory and the :class:`Symfony\\Component\\Httpkernel\\HttpKernel`
will no longer be able to fall back to the :class:`Symfony\\Component\\Httpkernel\\Controller\\ControllerResolver`.

.. seealso::

There is a wonderful tutorial series on using the HttpKernel component and
Expand Down Expand Up @@ -225,13 +237,21 @@ This implementation is explained more in the sidebar below::
public function getArguments(Request $request, $controller);
}

.. caution::

The ``getArguments()`` method in the :class:`Symfony\\Component\\Httpkernel\\Controller\\ControllerResolver`
and respective interface :class:`Symfony\\Component\\Httpkernel\\Controller\\ControllerResolverInterface`
are deprecated as of 3.1 and will be removed in 4.0. You can use the
:class:`Symfony\\Component\\Httpkernel\\Controller\\ArgumentResolver` which uses the
:class:`Symfony\\Component\\Httpkernel\\Controller\\ArgumentResolverInterface` instead.

Internally, the ``HttpKernel::handle`` method first calls
:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getController`
on the controller resolver. This method is passed the ``Request`` and is responsible
for somehow determining and returning a PHP callable (the controller) based
on the request's information.

The second method, :method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments`,
The second method, :method:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface::getArguments`,
will be called after another event - ``kernel.controller`` - is dispatched.

.. sidebar:: Resolving the Controller in the Symfony Framework
Expand Down Expand Up @@ -310,11 +330,11 @@ on the event object that's passed to listeners on this event.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Next, ``HttpKernel::handle`` calls
:method:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface::getArguments`.
:method:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface::getArguments`.
Remember that the controller returned in ``getController`` is a callable.
The purpose of ``getArguments`` is to return the array of arguments that
should be passed to that controller. Exactly how this is done is completely
up to your design, though the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver`
up to your design, though the built-in :class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver`
is a good example.

.. image:: /images/components/http_kernel/07-controller-arguments.png
Expand All @@ -326,7 +346,7 @@ of arguments that should be passed when executing that callable.
.. sidebar:: Getting the Controller Arguments in the Symfony Framework

Now that you know exactly what the controller callable (usually a method
inside a controller object) is, the ``ControllerResolver`` uses `reflection`_
inside a controller object) is, the ``ArgumentResolver`` uses `reflection`_
on the callable to return an array of the *names* of each of the arguments.
It then iterates over each of these arguments and uses the following tricks
to determine which value should be passed for each argument:
Expand All @@ -338,8 +358,19 @@ of arguments that should be passed when executing that callable.
from the ``RouterListener``).

b) If the argument in the controller is type-hinted with Symfony's
:class:`Symfony\\Component\\HttpFoundation\\Request` object, then the
``Request`` is passed in as the value.
:class:`Symfony\\Component\\HttpFoundation\\Request` object, the
``Request`` is passed in as the value. If you have a custom ``Request``
class, it will be injected as long as you extend the Symfony ``Request``.

c) If the function or method argument is `variadic`_ and the ``Request``
``attributes`` bag contains and array for that argument, they will all be
available through the `variadic`_ argument.

This functionality is provided by resolvers implementing the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentValueResolverInterface`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this interface be in the Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver\\ArgumentValueResolverInterface namespace with the other argument value resolvers ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface is in the Controller namespace

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know :) I was asking in the wrong place if it was not the wrong namespace in the PR. It could make sense to keep this interface in the same namespace as the implementations, WDYT ?

There are four implementations which provide the default behavior of Symfony but
customization is the key here. By implementing the ``ArgumentValueResolverInterface``
yourself and passing this to the ``ArgumentResolver``, you can extend this functionality.

.. _component-http-kernel-calling-controller:

Expand Down Expand Up @@ -612,47 +643,52 @@ A full Working Example
----------------------

When using the HttpKernel component, you're free to attach any listeners
to the core events and use any controller resolver that implements the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface`.
However, the HttpKernel component comes with some built-in listeners and
a built-in ControllerResolver that can be used to create a working example::
to the core events, use any controller resolver that implements the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface` and
use any argument resolver that implements the
:class:`Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So could this one

However, the HttpKernel component comes with some built-in listeners and everything
else that can be used to create a working example::

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;

$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
'_controller' => function (Request $request) {
return new Response(
sprintf("Hello %s", $request->get('name'))
);
}
)
));
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$request = Request::createFromGlobals();
$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
'_controller' => function (Request $request) {
return new Response(
sprintf("Hello %s", $request->get('name'))
);
})
));

$matcher = new UrlMatcher($routes, new RequestContext());
$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));
$matcher = new UrlMatcher($routes, new RequestContext());

$resolver = new ControllerResolver();
$kernel = new HttpKernel($dispatcher, $resolver);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));

$response = $kernel->handle($request);
$response->send();
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

$kernel->terminate($request, $response);

.. _http-kernel-sub-requests:

Expand Down Expand Up @@ -716,3 +752,4 @@ look like this::
.. _`@ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
.. _`@Template`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/view.html
.. _`EmailSenderListener`: https://github.com/symfony/swiftmailer-bundle/blob/master/EventListener/EmailSenderListener.php
.. _variadic: http://php.net/manual/en/functions.arguments.php
19 changes: 14 additions & 5 deletions create_framework/dependency_injection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ to it::
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;

class Framework extends HttpKernel\HttpKernel
{
public function __construct($routes)
{
$context = new Routing\RequestContext();
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$resolver = new HttpKernel\Controller\ControllerResolver();

$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));
$dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));

parent::__construct($dispatcher, $resolver);
parent::__construct($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);
}
}

Expand Down Expand Up @@ -101,7 +104,8 @@ Create a new file to host the dependency injection container configuration::
->setArguments(array($routes, new Reference('context')))
;
$sc->register('request_stack', 'Symfony\Component\HttpFoundation\RequestStack');
$sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');
$sc->register('controller_resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');
$sc->register('argument_resolver', 'Symfony\Component\HttpKernel\Controller\ArgumentResolver');

$sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
->setArguments(array(new Reference('matcher'), new Reference('request_stack')))
Expand All @@ -118,7 +122,12 @@ Create a new file to host the dependency injection container configuration::
->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
;
$sc->register('framework', 'Simplex\Framework')
->setArguments(array(new Reference('dispatcher'), new Reference('resolver')))
->setArguments(array(
new Reference('dispatcher'),
new Reference('controller_resolver'),
new Reference('request_stack'),
new Reference('argument_resolver'),
))
;

return $sc;
Expand Down
23 changes: 13 additions & 10 deletions create_framework/event_dispatcher.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,27 @@ the Response instance::
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;

class Framework
{
private $matcher;
private $resolver;
private $dispatcher;
private $matcher;
private $controllerResolver;
private $argumentResolver;

public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $resolver)
public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $controllerResolver, ArgumentResolverInterface $argumentResolver)
{
$this->matcher = $matcher;
$this->resolver = $resolver;
$this->dispatcher = $dispatcher;
$this->matcher = $matcher;
$this->controllerResolver = $controllerResolver;
$this->argumentResolver = $argumentResolver;
}

public function handle(Request $request)
Expand All @@ -63,8 +66,8 @@ the Response instance::
try {
$request->attributes->add($this->matcher->match($request->getPathInfo()));

$controller = $this->resolver->getController($request);
$arguments = $this->resolver->getArguments($request, $controller);
$controller = $this->controllerResolver->getController($request);
$arguments = $this->argumentResolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);
} catch (ResourceNotFoundException $e) {
Expand Down
50 changes: 31 additions & 19 deletions create_framework/http_kernel_controller_resolver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ component:

$ composer require symfony/http-kernel

The HttpKernel component has many interesting features, but the one we need
right now is the *controller resolver*. A controller resolver knows how to
determine the controller to execute and the arguments to pass to it, based on
a Request object. All controller resolvers implement the following interface::
The HttpKernel component has many interesting features, but the ones we need
right now are the *controller resolver* and *argument resolver*. A controller resolver knows how to
determine the controller to execute and the argument resolver determines the arguments to pass to it,
based on a Request object. All controller resolvers implement the following interface::

namespace Symfony\Component\HttpKernel\Controller;

Expand All @@ -58,6 +58,14 @@ a Request object. All controller resolvers implement the following interface::
function getArguments(Request $request, $controller);
}

.. caution::

The ``getArguments()`` method in the :class:`Symfony\\Component\\Httpkernel\\Controller\\ControllerResolver`
and respective interface :class:`Symfony\\Component\\Httpkernel\\Controller\\ControllerResolverInterface`
are deprecated as of 3.1 and will be removed in 4.0. You can use the
:class:`Symfony\\Component\\Httpkernel\\Controller\\ArgumentResolver` which uses the
:class:`Symfony\\Component\\Httpkernel\\Controller\\ArgumentResolverInterface` instead.

The ``getController()`` method relies on the same convention as the one we
have defined earlier: the ``_controller`` request attribute must contain the
controller associated with the Request. Besides the built-in PHP callbacks,
Expand All @@ -74,10 +82,11 @@ resolver from HttpKernel::

use Symfony\Component\HttpKernel;

$resolver = new HttpKernel\Controller\ControllerResolver();
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();

$controller = $resolver->getController($request);
$arguments = $resolver->getArguments($request, $controller);
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);

Expand Down Expand Up @@ -140,14 +149,12 @@ method is not defined, an argument has no matching attribute, ...).

.. note::

With the great flexibility of the default controller resolver, you might
wonder why someone would want to create another one (why would there be an
interface if not?). Two examples: in Symfony, ``getController()`` is
enhanced to support
:doc:`controllers as services </cookbook/controller/service>`; and in
`FrameworkExtraBundle`_, ``getArguments()`` is enhanced to support
parameter converters, where request attributes are converted to objects
automatically.
With the great flexibility of the default controller resolver and argument
resolver, you might wonder why someone would want to create another one
(why would there be an interface if not?). Two examples: in Symfony,
``getController()`` is enhanced to support :doc:`controllers as services </cookbook/controller/service>`;
and ``getArguments()`` provides an extension point to alter or enhance
the resolving of arguments.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you intend to re-document the param converters later, once the works in progress are done, we should maybe open an issue to not forget that ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Param converters will keep working the way they do, I think it's easier to re-document that when they change. The reason I have changed the notion of getArgument() here, is because it does not exist in this class at all.


Let's conclude with the new version of our framework::

Expand All @@ -174,13 +181,18 @@ Let's conclude with the new version of our framework::
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$resolver = new HttpKernel\Controller\ControllerResolver();

$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();

$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);

try {
$request->attributes->add($matcher->match($request->getPathInfo()));

$controller = $resolver->getController($request);
$arguments = $resolver->getArguments($request, $controller);
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);

$response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $e) {
Expand All @@ -192,7 +204,7 @@ Let's conclude with the new version of our framework::
$response->send();

Think about it once more: our framework is more robust and more flexible than
ever and it still has less than 40 lines of code.
ever and it still has less than 50 lines of code.

.. _`reflection`: http://php.net/reflection
.. _`FrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
Expand Down
Loading