index.php to HttpKernel::handle()
Let's start from the very beginning of the request. When we load a page, the first file that's executed is public/index.php
. No matter what, this is where it all starts. So let's literally go through this file line-by-line and see what happens.
Tip
If you start a new project in Symfony 5.3 or later, this file will look quite different thanks
to the new symfony/runtime
component. But, the same things are still happening behind
the scenes.
index.php Bootstrapping
The first thing it does is require this config/bootstrap.php
file. For our purposes, this... isn't important. It requires the Composer autoloader... and then the rest of this file is all about loading and normalizing environment variables. Sure, environment variables are important to Symfony, but if you want to understand the request-response flow, not so much.
Next, if we're in debug mode, it calls Debug::enable()
. That's great to set up some debugging tools... but not relevant to us.
Hello Kernel
The first thing we care about is down here: $kernel = new Kernel()
. This is actually instantiating our src/Kernel.php
class, which is the heart of our application.
The Kernel
is passed the environment as the first argument and a debug flag as the second. That controls a bunch of behavior... but isn't very important to the request-response flow.
But the next line is important. We always knew that there was a Request
object inside Symfony. If you ever wondered who creates the Request
and where, here's your answer: it's created in our code - not somewhere deep in the core.
The ::createFromGlobals()
method - I'll hold command or control to open that method inside Symfony - is a shortcut to create the Request
object and populate its data with the normal superglobal variables, like $_SERVER
and $_POST
. This gives us a nice Request
object that represents the current request info.
HttpKernel::handle(): Our App in One Method
The next line... oh... the next line. This is probably my favorite line of code in all of PHP: $response = $kernel->handle($request)
. That runs our app. We don't know exactly what happens inside that method - that's what we're going to figure out - but isn't it beautiful? Our application & Symfony are not some weird, global monster that takes over our PHP process and eats our objects. Nope, it's a pure function. Input $request
, output $response
... which is exactly what our job as a developer is! Understand the incoming request, and use that to create a response.
One of the properties of a "pure" function like this is that you can call it as many times as you want. So yes, in theory, a single Kernel
can handle multiple requests inside just one PHP process. In fact, let's do that!
Up above, let's say $request1 = Request::create()
- which is another shortcut to create a Request
object. Let's make this look like a Request for our login page. Pass /login
as the first arg.
Now create a $request2
variable and pretend that this is a request for /register
.
// ... lines 1 - 24 | |
$request1 = Request::create('/login'); | |
$request2 = Request::create('/register'); | |
// ... lines 27 - 39 |
Could we run our kernel and get 2 responses for these 2 requests? Uh... totally! $response1 = $kernel->handle($request1)
... and then $response2 = $kernel->handle($request2)
. Let's see what they look like: dump($response1)
, dump($response2)
and then die
.
// ... lines 1 - 27 | |
$response1 = $kernel->handle($request1); | |
$response2 = $kernel->handle($request2); | |
dump($response1); | |
dump($response2); | |
die; | |
// ... lines 34 - 39 |
Let's do this! Move over, refresh and... check it out! We just handled two different requests on the same page! The first does contain the HTML for the login page, and the second... for the registration page. Amazing.
And this idea of handling multiple requests in Symfony is something that really does happen! It happens with sub-requests - a topic that we will cover later in this tutorial - and some people use an event loop in PHP to boot a single kernel and then handle many, real, HTTP requests.
Ok, remove all of this code. It's now obvious that if we really want to understand what happens inside Symfony, we need to find out what happens inside of this $kernel->handle()
method. We're going be opening a lot of core files, so make sure you have an easy way to "jump to a file" by typing a filename in your editor. In PhpStorm, I can hit Shift+Shift to open a file called HttpKernel.php
, which lives deep inside Symfony. If you don't see it, make sure the "Include non-project items" checkbox is checked - PhpStorm usually does that automatically if you type a specific filename.
Once inside... scroll down to the handle()
method.
Hello HttpKernel::handle()
Ok, technically the $kernel->handle()
method we saw in index.php
is not the handle()
method in this class. Symfony first initializes the dependency injection container - the topic of a future deep-dive tutorial - and then calls this method.
The first thing I want you to notice is that the entire function is surrounded by a try-catch block. So almost immediately when our app starts running, our code is surrounded by a try catch! That's not important yet. But later, we'll see what happens when an exception is thrown from anywhere.
The real logic of HttpKernel
lives in this handleRaw()
method. Scroll down a little to find it. Ah yes: handleRaw()
. This is the Symfony framework. These 50 lines of code are the heart of everything that happens in Symfony! And not just Symfony: these same 50 lines of code run Drupal, phpBB and many other things!
So next: let's start our journey through this strange and wondrous method.
Hey @weaverryan and Team!
I've been diving deeeep into Symfony trying to load in some params from a 3rd party via an API request and then set these params at run time. I tried an onKernelRequest Subscriber and had the API request working etc, but... can't set the params here due to "Impossible to call set() on a frozen ParameterBag." error. So then I created a CompilarPass() class to try and get the params and set them during the container build process. But I run into a dependency injection loop where I need the RequestStack and my other FetchParams service in my CustomCompilarPass class but they aren't "built" in the container yet? So when I instantiate my CustomCompilarPass() in the kernel I actually need to inject my FetchParams service & RequestStack. I tried registering everything as services but I don't think they are available yet? Anyhow, just wondering if you had any advice on what to tap into to make an api request and set / modify some params at runtime. I know env file or environment variables are probably preferred but have an interesting use case. Thanks! Jon