Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Controller Resolver

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

After the kernel.request event is dispatched, it checks if $event->hasResponse() and, if that's true... it returns the response! Well, it calls $this->filterResponse() and returns that - but that's not too important. We'll see that method later. The point is: the function ends. The response is returned immediately before our controller is executed or anything else.

Listeners to kernel.request can Return a Response

This is kinda cool. If a listener to kernel.request somehow already has enough information to return a response... it can do that! It's not super common, it could be used for security or a maintenance page... but hey! Let's try it ourselves!

In src/EventListener/UserAgentSubscriber.php, we can say $event->setResponse(). Not all event classes have this setResponse() method - but RequestEvent does. Then say new Response() and set a very important message.

... lines 1 - 6
use Symfony\Component\HttpFoundation\Response;
... lines 8 - 9
class UserAgentSubscriber implements EventSubscriberInterface
{
... lines 12 - 18
public function onKernelRequest(RequestEvent $event)
{
$event->setResponse(new Response(
'Ah, ah, ah: you didn\'t say the magic word'
));
... lines 24 - 28
}
... lines 30 - 36
}

Now when we refresh... yay! Nedry killed every page!

Remove that code... and then I'll close a few files.

The Controller Resolver

Back in HttpKernel, so far we've dispatch one event. RouterListener listened to that event and modified the request attributes.

Let's keep following the code. This next line is interesting: inside the if we have: $controller = $this->resolver->getController(). This "resolver" thing is a "controller resolver". At a high level, it's beautiful. We know that we will eventually need to execute a controller that will create the Response. This class is entirely responsible for determining that controller: we pass it the request and - somehow - it gives us back a controller, which can be any callable.

What Class is the ControllerResolver?

How can we figure out what class $this->resolver is? Well... of course, there's always the handy dd($this->resolver), which... tells us that this is an instance of TraceableControllerResolver. By the way, whenever you see a class called "Traceable" something, this is almost definitely an object that is decorating the real object in the dev environment in order to give us some debugging info. The real controller resolver is inside: Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver.

Another way to figure this out - maybe a slightly nerdier way for a deep-dive course - is with smart use of the debug:container command. The $resolver is one of the arguments to the constructor on top: it's argument 2. We can see the interface type-hint, but not the concrete class.

Scroll back down, then move over to your terminal and run php bin/console debug:container http_kernel - that's the service id of HttpKernel - --show-arguments.

This tells me that the second argument is debug.controller_resolver. Ok, let's run this command again to get more info about that:

php bin/console debug:container debug.controller_resolver --show-arguments

This uses Symfony's service decoration - a topic we'll see in our next deep-dive tutorial. But basically, when a service is decorated, the original, "inner" service's id will be debug.controller_resolver.inner. So... run debug:container with that!

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

And here it is: the true controller resolver class is... what we already know: it's called ControllerResolver and lives inside FrameworkBundle.

Opening the ControllerResolver Classes

So... let's open that up and see what it looks like! I'll hit Shift+Shift and look for ControllerResolver.php. Oh, there are two of them: the one from FrameworkBundle and another from HttpKernel. So... there's some inheritance going on: the ControllerResolver from FrameworkBundle extends a ContainerControllerResolver... which extends the ControllerResolver from HttpKernel. The class from FrameworkBundle doesn't contain anything important that we need to look at. So I'm actually going to open ContainerControllerResolver first. And... yep! Its base class is ControllerResolver, which lives in the same namespace. Hold Command or Ctrl and click that class to open it.

Hello ControllerResolver

Ok, time to see what's going on! HttpKernel called the getController() method. Let's go see what that looks like!

The getController() method is passed the Request and... oh! Check it out! The first thing it does is fetch _controller from the request attributes! So why is _controller the "magic" key you can use in your YAML route? It's because of this line right here: the ControllerResolver looks for _controller.

Ultimately, what this method needs to return is some sort of callable. For us, it will be a method inside an object, but it can also be a number of other things, like an anonymous function. Let's see what our $controller looks like at this point. dd($controller), then move back to your browser and refresh.

... lines 1 - 23
class ControllerResolver implements ControllerResolverInterface
{
... lines 26 - 35
public function getController(Request $request)
{
... lines 38 - 45
dd($controller);
... lines 47 - 96
}
... lines 98 - 222

Ah yes: for us, $controller is the normal string format we've been seeing: the full controller class, ::, then the method name.

Remove the dd() and let's trace down on the code. This has a bunch of if statements, which are all basically trying to figure out if the controller is maybe already a callable. It checks if it's an array and if the 0 and 1 elements are set - because that's a callable format. We're also not an object... and our string is not a function name. Basically, our controller is not already a "callable".

So ultimately, we fall down to $callable = $this->createController(). Somehow this function converts our string into something that can be invoked. How? Let's find out next.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works well for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.3
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2.0", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.2.2
        "doctrine/orm": "^2.5.11", // 2.8.2
        "easycorp/easy-log-handler": "^1.0", // v1.0.9
        "http-interop/http-factory-guzzle": "^1.0", // 1.0.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.9.0
        "knplabs/knp-paginator-bundle": "^5.0", // v5.4.2
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.1
        "knplabs/knp-time-bundle": "^1.8", // v1.16.0
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.24
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.9.1
        "liip/imagine-bundle": "^2.1", // 2.5.0
        "oneup/flysystem-bundle": "^3.0", // 3.7.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.2
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.1", // v5.6.1
        "symfony/asset": "5.0.*", // v5.0.11
        "symfony/console": "5.0.*", // v5.0.11
        "symfony/dotenv": "5.0.*", // v5.0.11
        "symfony/flex": "^1.9", // v1.17.5
        "symfony/form": "5.0.*", // v5.0.11
        "symfony/framework-bundle": "5.0.*", // v5.0.11
        "symfony/mailer": "5.0.*", // v5.0.11
        "symfony/messenger": "5.0.*", // v5.0.11
        "symfony/monolog-bundle": "^3.5", // v3.6.0
        "symfony/property-access": "5.0.*|| 5.1.*", // v5.1.11
        "symfony/property-info": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/routing": "5.1.*", // v5.1.11
        "symfony/security-bundle": "5.0.*", // v5.0.11
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.11
        "symfony/serializer": "5.0.*|| 5.1.*", // v5.1.10
        "symfony/twig-bundle": "5.0.*", // v5.0.11
        "symfony/validator": "5.0.*", // v5.0.11
        "symfony/webpack-encore-bundle": "^1.4", // v1.11.1
        "symfony/yaml": "5.0.*", // v5.0.11
        "twig/cssinliner-extra": "^2.12", // v2.14.3
        "twig/extensions": "^1.5", // v1.5.4
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.0
        "twig/inky-extra": "^2.12", // v2.14.3
        "twig/twig": "^2.12|^3.0" // v2.14.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.4.0
        "fakerphp/faker": "^1.13", // v1.13.0
        "symfony/browser-kit": "5.0.*", // v5.0.11
        "symfony/debug-bundle": "5.0.*", // v5.0.11
        "symfony/maker-bundle": "^1.0", // v1.29.1
        "symfony/phpunit-bridge": "5.0.*", // v5.0.11
        "symfony/stopwatch": "^5.1", // v5.1.11
        "symfony/var-dumper": "5.0.*", // v5.0.11
        "symfony/web-profiler-bundle": "^5.0" // v5.0.11
    }
}
userVoice