Skip to content

Commit

Permalink
Documented the ArgumentResolver along the ControllerResolver
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltar van der Berg committed Apr 11, 2016
1 parent d7724dd commit 24b11e2
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 92 deletions.
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`.
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`.
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.

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

0 comments on commit 24b11e2

Please sign in to comment.