Routing Secrets & Request Attributes

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.

This array is the end result of the route-matching process. Apparently, the router returns an array with the wildcard values from the route plus keys for the route and controller.

But... it's a bit more interesting than that. A great way to see how, is by playing with a route in YAML. Open up config/routes.yaml. Uncomment the example route and change the path to /playing. Now, on your browser, open another tab and go to https://localhost:8000/playing.

index:
path: /playing
... lines 3 - 4

That's exactly what we expected: _route set to the route name and _controller set to the controller string for that route.

Route Defaults

But in reality, the controller key in a YAML route is just a shortcut. Before Symfony 4, there was no controller key. Nope, to define a controller you added a defaults key and put an _controller key below that.

index:
... line 2
defaults:
_controller: App\Controller\DefaultController::index

Move over and refresh now. Woh! We get the exact same array! Yep, the controller key is really just a shortcut for setting an _controller default value on the route.

This is actually an important point, but to see why, let's go a bit further. First, add a {id} wildcard to the end of the path. Then, at your browser, add /5 to the end of the URL. And... yep! The array now has an id key: no surprise.

index:
path: /playing/{id}
... lines 3 - 5

Normally, the purpose of defaults on a route are to give a default value for a wildcard. If we say id: 10... and then refresh, the array still contains 5 because that's what's in the URL. But thanks to the default, now we can just go to /playing and... the id uses the default value 10.

index:
... line 2
defaults:
... line 4
id: 10

Cool. But what if we just... invent a new key and put it here? Like totally_inventing_this_default set to true.

index:
... line 2
defaults:
... lines 4 - 5
totally_inventing_this_default: true

This won't change how the route matches, but it will change what we get back in the array. Refresh. The totally_inventing_this_default key is now inside the returned array!

So here's the full story of what the route matching process returns: it returns an array_merge of the route defaults and any wildcard values in the route.... plus the _route key... just in case that's handy.

With route annotations, it looks a bit different, but it's exactly the same. We can add a defaults key and set foo to bar. Back in the browser, close the last tab and refresh the article show page. We suddenly have a foo key! On the route, remove that defaults stuff.

... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 38
/**
* @Route("/news/{slug}", name="article_show", defaults={"foo": "bar"})
*/
public function show(Article $article, SlackClient $slack)
... lines 43 - 64
}

Request Attributes

So why is it so important to understand exactly what the route-matching process returns? We'll find out the full answer soon. But first... back in RouterListener, what does this class do with the $parameters array?

Remove the dd()... and let's follow the logic. It does some logging and... here it is: $request->attributes->add($parameters). This is important.

Let's back up for a second: the Request object has several public properties and all of them - except one! - correspond to something on the HTTP request. For example, $request->headers holds the HTTP request headers, $request->cookies holds the cookies, and there are others like $request->query to read the query parameters. The point is: all of these refer to real "parts" of an HTTP request. You could talk to a Java developer about HTTP headers and they would know what you're referring to.

The one exception is $request->attributes. This property does not correspond to any real part of the HTTP request. If you ask that same Java developer:

Hey! What are the attributes on your request?

They'll think you're nuts. Nope, the Request attributes are something totally invented by Symfony. The purpose of the request attributes is to be a place where you can store data about the request that's specific to your application. So, storing the controller, for example, is a perfect fit! That's completely a Symfony concept.

Anyways, the array of $parameters from the router is added to the $request->attributes(). What does that... do? Absolutely nothing. Soon, something else will use this data, but at this moment, this is just data sitting on the request.

It also sets another attribute _route_params... but that's not really important.

After kernel.request... we have Request Attributes!

Ok! RouterListener done! Close that class, high-five your cat - and go back to HttpKernel. As we saw, there are a lot of listeners to the kernel.request event, but by far, the most important one is RouterListener. So what changed in our system before and after this dispatch() line? Basically... just the request attributes.

In fact, let's see this. Above dispatch, dump($request->attributes->all(). Then copy that... dump after, and die. Refresh the article show page. Yep! Before we dispatch the event, the attributes are empty. After? We have _route, _controller, slug and hey! A few other things were added by other listeners related to security. That's not important for us - but still, interesting!

... lines 1 - 39
class HttpKernel implements HttpKernelInterface, TerminableInterface
{
... lines 42 - 114
private function handleRaw(Request $request, int $type = self::MASTER_REQUEST): Response
{
... lines 117 - 120
dump($request->attributes->all());
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
dump($request->attributes->all());
die;
... lines 125 - 169
}
... lines 171 - 284
}

Remove all that debug code.

Seeing the Dumped Route

Before we find out how the request attributes are used, I want to show you something kinda cool. We're going to look at a cache file: var/cache/dev... and then url_matching_routes.php.

This file is automatically generated by Symfony and is the end-result of all of the routes in our application. This file is insane. After reading our routes, Symfony generates a huge list of regular expressions and which route should match which part, and dumps them to this file. This is used by the route-matching process so that it's blazingly fast. It's... pretty amazing.

Anyways, next! Let's see the significance of those Request attributes by continuing to go through the handleRaw() method.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.3.0",
        "ext-iconv": "*",
        "antishov/doctrine-extensions-bundle": "^1.4", // v1.4.2
        "aws/aws-sdk-php": "^3.87", // 3.133.20
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.0", // 2.0.7
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // 2.1.2
        "doctrine/orm": "^2.5.11", // v2.7.1
        "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.8.1
        "knplabs/knp-paginator-bundle": "^5.0", // v5.1.1
        "knplabs/knp-snappy-bundle": "^1.6", // v1.7.0
        "knplabs/knp-time-bundle": "^1.8", // v1.11.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.3.0
        "nexylan/slack-bundle": "^2.1", // v2.2.2
        "oneup/flysystem-bundle": "^3.0", // 3.4.0
        "php-http/guzzle6-adapter": "^2.0", // v2.0.1
        "sensio/framework-extra-bundle": "^5.1", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.4
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.9", // v1.9.10
        "symfony/form": "5.0.*", // v5.0.4
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/mailer": "5.0.*", // v5.0.4
        "symfony/messenger": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.5", // v3.5.0
        "symfony/security-bundle": "5.0.*", // v5.0.4
        "symfony/sendgrid-mailer": "5.0.*", // v5.0.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "5.0.*", // v5.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.7.3
        "symfony/yaml": "5.0.*", // v5.0.4
        "twig/cssinliner-extra": "^2.12", // v2.12.5
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.3.0
        "fzaninotto/faker": "^1.7", // v1.9.1
        "symfony/browser-kit": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/maker-bundle": "^1.0", // v1.14.3
        "symfony/phpunit-bridge": "5.0.*", // v5.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "5.0.*" // v5.0.4
    }
}