This tutorial has a new version, check it out!

Symfony Magic: Replace the _controller

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $9.00

Symfony Magic: Replace the _controller

If this were the Matrix, you’d be seeing 1’s and 0’s running across your code. Your Symfony world is now one without rules or controls, without borders or boundaries. Let’s prove it.

We know that Symfony reads the _controller key in the request attributes and executes that as the controller. This is set by the RouterListener but it could be set anywhere.

Let’s go adventuring!. UserAgentSubscriber listens to kernel.request. What if we just replaced the _controller key here with something else? Let’s do that, and set it to an anonymous function:

// src/AppBundle/EventListener/UserAgentSubscriber.php
// ...
use Symfony\Component\HttpFoundation\Response;

public function onKernelRequest(GetResponseEvent $event)
{
    // ...

    $request->attributes->set('_controller', function() {
        return new Response('Hello Dinosaur!');
    });
}

Symfony doesn’t see any difference between this function and a traditional controller. To prove it, return a normal Response:

return new Response('Hello world!')

When we refresh, would you believe it? That actually works! Comment this line out.

Forcing RouterListener NOT to Route

Even within listeners to a single event, there is an order of things. In the events tab of the Profiler, you can see that our listener is the last one that’s called by kernel.request. The RouterListener is executed before us. That means that the routing is being run and the _controller key is being set. Later, our listener replaces it.

But even if our listener were run before the router, our little hack would still work. That’s because RouterListener has a special piece of code near the top that checks to see if the _controller key is already set. If it is set somehow, the routing never runs:

// vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/RouterListener.php
// ...

public function onKernelRequest(GetResponseEvent $event)
{
    // ...

    if ($request->attributes->has('_controller')) {
        // routing is already done
        return;
    }

    // ...
}

That’ll be important when we talk about sub-requests.

And since our anonymous function is a valid controller, we can still use arguments on it just like normal. So if I go to /dinosaurs/22, which has a {id} in the route, we’re allowed to have an $id argument:

// src/AppBundle/EventListener/UserAgentSubscriber.php
// ...
use Symfony\Component\HttpFoundation\Response;

public function onKernelRequest(GetResponseEvent $event)
{
    // ...

    $request->attributes->set('_controller', function($id) {
        return new Response('Hello '.$id);
    });
}

So this will work just fine. All controllers are created equal.

Now, let’s comment that out temporarily, or more likely permanently:

// src/AppBundle/EventListener/UserAgentSubscriber.php
// ...
use Symfony\Component\HttpFoundation\Response;

public function onKernelRequest(GetResponseEvent $event)
{
    // ...

    /*
    $request->attributes->set('_controller', function($id) {
        return new Response('Hello '.$id);
    });
    */
}

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.x-dev", // 2.6.x-dev
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.5.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.7
        "symfony/monolog-bundle": "~2.4", // v2.6.1
        "sensio/distribution-bundle": "~3.0", // v3.0.9
        "sensio/framework-extra-bundle": "~3.0", // v3.0.3
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "~0.2" // 0.2
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.4.0
    }
}