If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThere are several other interesting listeners to kernel.response
. Here's one: ContextListener
... and it's from security! Open that up: Shift+Shift, ContextListener.php
.
Scroll down to find the method we care about: onKernelResponse()
. It says:
Writes the security token into the session.
If you use a "stateful" firewall... which you probably are, unless your security system is a pure, API token-based system, then this is the class that's responsible for taking your authenticated User
object - technically the "token" object that holds it - and saving it into the session. Here it is: $session->set($this->sessionKey, serialize($token))
.
This class is also responsible for unserializing the token at the start of each request - that's in a different method.
Close this class and look back at the event list. Let's see... there's a listener called DisallowRobotsIndexingListener
, which adds an X-Robots-Tag
header set to noindex
if you set a framework.disallow_search_engine_index
option to true. Phew! That option defaults to true
in dev... which is why we see this. So if you... accidentally... deploy your site in dev mode, it won't be indexed.
Let's look at one more: SessionListener
. Open that one up: Shift+Shift then SessionListener.php
.
This class is responsible for actually storing the session information. It extends AbstractSessionListener
... which holds the majority of the logic
This also listens on the kernel.request
event... but we're interested in the onKernelResponse()
method. It does several things... but eventually, it calls $session->save()
to actually put your session data into storage. All these tiny invisible pieces help make your application sing.
Ok, enough playing with these listeners. Close the two session classes and go back to HttpKernel
. After dispatching the kernel.response
event, this calls a finishRequest()
method and then finally returns the Response
that's on the event. Let's see what finishRequest()
does. Ah! It dispatches one more event and then calls $this->requestStack->pop()
.
Remember: this RequestStack
object is basically a collection of request objects - something we'll talk more about soon. The pop()
method removes the most-recently-added Request
object from that collection. If you scroll back up to the top of handleRaw()
, the pop()
call does the opposite of $this->requestStack->push($request)
. So... we don't know why this request stack thing needs to exist... but we at least know that the current Request
object is added to the RequestStack
at the beginning of handling the request, and then removed at the end.
So... we're done! The filterResponse()
method returns the Response
, then handleRaw()
returns the same Response
... and then handle()
also returns the Response
... all the way back to index.php
: $response = $kernel->handle($request)
.
We made it! But we haven't sent anything to the user yet: everything is still just stored in PHP memory. The next call takes care of that: $response->send()
. I'll open that up. It's just a fancy way of calling the PHP header()
function to set all the headers and then echo'ing the content. At this point, our response is sent!
Back in index.php
, there's one final line: $kernel->terminate()
. Let's find that inside of HttpKernel
. And... wow. I'm personally shocked. This dispatches one final event.
This event is dispatched so late that... if your web server is set up correctly, the response has already been sent to the user! This event isn't used too often... but it is where all the data for the profiler is stored, for example. In fact, that's the only listener to this event: ProfilerListener
.
So that is Symfony's request-response process in depth. A work of art.
It may have seemed like a lot, but if you zoom out, it's delightfully simple: we dispatch an event, find the controller, dispatch an event, find the arguments, dispatch an event, call the controller, dispatch 2 more events in filterResponse()
and finishRequest()
and then, back in index.php
, we send the headers, echo the content and dispatch one last event. It's... kind of a "find the controller, call the controller" system... with a ton of events mixed in as hook points.
But go back to HttpKernel
and scroll all the way back up to handle()
. Ah yes, this wraps all of our code in a try-catch block. So what happens if an exception is thrown from somewhere in our app? Well, quite a lot. Let's jump into that next.
Hey sridharpandu!
Definitely you can set this in a controller - you can set it directly on the Response object. For example:
/**
* @Route("/products")
*/
public function products()
{
// ...
$response = $this->render('product/products.html.twig');
$response->setExpires(new \DateTime('+24 hours'));
return $response;
}
Of course, to fully take advantage of this, you'll need some sort of HTTP cache (e.g. Varnish) installed on your server to cache this. But strictly speaking, this is how you control that header.
Cheers!
I am working on setting up the Varnsih Cache for one of our Drupal 8 Sites. It seems to be caching the assets but the home page is set to expire on 19th Nov 1978 (and is hence uncacheable) which I belive is Dries DOB! But tfound some literature on the Drupal.org sites. Still a lot more work to do. However we will primarily be using the API Platfrom for development.
Hey sridharpandu!
> but the home page is set to expire on 19th Nov 1978 (and is hence uncacheable) which I belive is Dries DOB
If that's true, that's hilarious!
> But tfound some literature on the Drupal.org sites. Still a lot more work to do.
Good luck! As you know, Drupal adds a lot of layers and functionality on top of Symfony... and though I'm not familiar with these specific layers in Drupal, they almost certainly do things with these cache headers.
Cheers!
// 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
}
}
I see that the expires header is set with the following line
setExpires(new \DateTime());
Is there a way to set this to a date in the future, in the controllers that we write or do we have to set it in the webserver? This is required especially on sites where we output basic product or profile pages that seldom change.