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 SubscribeNo matter what, Symfony's job is to look at the incoming request and somehow convert that into a response. Well, that's really the job of HttpKernel
, specifically the handleRaw()
method.
At this point we have a Response
object. Yay! Either our controller returned the response or a kernel.view
listener created it. Then, at the bottom, our last act is to return $this->filterResponse($response)
.
Before we look at that method, there was one other place during this process where we could have already returned a response. Scroll to the top. Here: listeners to the very first event - kernel.request
- have the ability to set a Response
immediately. If they do, this also calls $this->filterResponse()
.
The point is: no matter how we get the Response
, eventually, filterResponse()
will be called. Hold Command or Ctrl and click that method to jump to it. What a shock! This method dispatches yet another event! This one is called KernelEvents::RESPONSE
, which is the string kernel.response
.
There are several interesting listeners to this event. Go back to your browser, refresh the page, click to go into the profiler and then into the Events section. Way down... here it is: kernel.response
.
There are many cool, subtle, listeners to this event, like the WebDebugToolbarListener
! Listeners to this event have access to the final Response
object. I'm going to open the original page in a new tab... and view the HTML source. Very simply, the WebDebugToolbarListener
checks to see if this is a full HTML page and, if it is, it modifies the HTML in the Response
and adds a bunch of JavaScript at the bottom. This JavaScript is what's responsible for rendering the web debug toolbar. That's such a cool example.
As far as understanding the mechanics of the request-response flow, there are actually no critical listeners to this event... but there are still some pretty important ones, like ResponseListener
. Let's open that one up.
I'll hit Shift+Shift to open ResponseListener.php
: get the one from http-kernel/
, not security
. It says:
ResponseListener
fixes theResponse
headers based on theRequest
.
Let's... find out what that means. Inside onKernelResponse()
... the most important part is that this calls $response->prepare($event->getRequest())
. Ok! Hold Command or Ctrl to jump into that.
Here's the idea: once the Response
has been created, this checks the Response
to see if it has some missing pieces. For example, if the response doesn't have a Content-Type
header yet - if that's not something that we set in the controller - this uses some information from the request to set that header for you.
This touches on an important, but subtle detail inside Symfony. I'm curious how the getPreferredFormat()
method works. Hold Command or Ctrl to jump into that.
I won't go into the full details of this method, but here are the basics. This method's job is to try to determine what format - like html
or json
- that the client - the "thing" that's sending the request - prefers for the response. Basically: does the client want an HTML response, a JSON response or something else?
To figure that out, it does two things. First, it calls $this->getRequestFormat()
. Basically, it checks to see if something else has explicitly said:
This is definitely the format that the user is requesting
This can be set in 2 different ways. First, by calling $request->setRequestFormat()
, which you could do, for example, in a listener. At this time, there is only one place in all of core that calls this function: the json_login
authentication system calls $request->setRequestFormat('json')
.
The second way the request format can be set is by putting an _format
key into the request attributes! That most commonly happens by adding an _format
default to your route or by actually having an {_format}
route wildcard.
If neither of those things happens - which is the "normal" way that things work - then this method next loops over the "acceptable content types" to find one that works. This reads the Accept
header on the Request
, loops over them with the highest priority format first, and, as soon as it finds one that it understands, that's returned.
The big point here is this: Symfony has a concept of "what response format
does this request prefer?". It uses that to set the Content-Type
header. This idea is going to be important again later with error handling.
By the way, this line may change to use $request->getRequestFormat()
at some point in the future... which just means that if you like this idea of it automatically setting the Content-Type
response header based on the Accept
request header, make sure you do it explicitly when you create your Response
.
Close the Response
class and ResponseListener
. Before we boldly push forward, there are a few other listeners I want to look at. Let's check those out next and then... yes... send the final response to the user! We're close.
"Houston: no signs of life"
Start the conversation!
// 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
}
}