Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

4
Login or Register to join the conversation
Nadine Avatar

Hi, i've some questions about the dispatching part. I see that the dispatch function has to return data, but there is not return statement in the onKernelRequest function in the routerlistener file. How does that data coming back?

Also at 6:00 you're showing that the request object is changed, but how did that happened?

I can't make the connection that the modifications by the event dispatched are returned.

Reply

Hey Nadine !

Excellent question :). You're right that listener functions to an event (like onKernelRequest()) do not return anything. If they did, it would be totally ignored. Instead, if a listener needs to "communicate" something back to the original code that dispatched the event (in this case, `HttpKernel::handleRaw()</code), that's done by modifying some object. Specifically, this is usually done by modifying the event object itself. For example, here is the code from HttpKernel that dispatches the event:


// HttpKernel::handleRaw()
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);

The RequestEvent is passed to all listeners to this event... and listeners can modify that object by using any setter methods it may have. For example (RouterListener doesn't do this, but it's still a valid example), the RequestEvent has a setResponse() method. A listener can call that to set a Response on the event (i.e. it modifies the event). Then, the code in HttpKernel checks to see if someone has set a Response and uses it if one has - that's the next lines after dispatching:


// did someone modify the event and set a response?
if ($event->hasResponse()) {
    return $this->filterResponse($event->getResponse(), $request, $type);
}

So, there is no direct communication between who dispatches the event and the listeners. But listeners can communicate by modifying the Event object.

RouterListener does something very similar. It doesn't modify the Event itself, but it does modify the Request. Here's the code form HttpKernel next to the RouterListener code to see how this looks:


// HttpKernel::handleRaw()

// the Request object is passed to RequestEvent and is accessible via $event->getRequest() in listeners
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);

// RouterListener::onKernelRequest()

// .. after executing the routing, it modifies the Request object
$request->attributes->add($parameters);



So, quite literally, one of the most important results of the dispatching this event is that one listener (RouterListener) modifies/mutates the Request object. HttpKernel then goes on to use that new data on the Request to do other stuff.

Let me know if that makes sense!

Cheers!
Reply
Nadine Avatar

Thanks for your answer!

So the Event object is first passed to the listener with the highest priority. After excuthing the first listener function it goes to the next listener with the modified object from the first listener.

And the Event object is send as reference, so it doesn't have to be returned with a return statement.

Am I right?

Reply

Yep, you're right. In PHP all objects are passed by reference by default, that's why listeners do not have to returning anything, they just have to work with the event object

Cheers!

1 Reply
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