Symfony has many components and can do a lot of work. But in general, it just has the basic concept: handle the request and return the response. The whole flow is uncovered right in the index.php
:
Symfony 5 high-level flow
The index.php
is the first file to run in the Symfony process and have 3 main responsibilities:
- Create the
Request
object by using theRequest::createFromGlobals()
, this function will gather the PHP global variables ($_GET
,$_POST
,$_COOKIE
,$_FILES
,$_SERVER
) and store into theRequest
object. - Initialize the Kernel object which contains the
handle
method, use thehandle
method to consume theRequest
object which will return theResponse
object - Send the
Response
to client vie$response->send()
, this function will send theheader
via the PHPheader()
function and send thecontent
via the PHPecho
function.
That is the whole Symfony Request & Response flow at a very high level. Now you can answer the interview question “What is the flow of Symfony framework ?” and good luck! 😛
The interviewer will ask further questions to check your understanding, so let’s keep going. The (1) and (3) are quite straightforward, let’s dive deeper into the (2) to reveal the magic inside the handle
method. This is written in the vendor/symfony/http-kernel/HttpKernel.php
. I’ve just keep the most relevant code so we can focus on the main things:
The main part of handle function in HttpKernel.php in Symfony 5
The handle
method will call the handleRaw
method which process 4 stages:
- request
- load controller
- controller arguments
- response
Request stage
“Request” stage
At first glance, this code block add the request into the RequestStack
(thank to that, you can get the Request
inside your service
by injecting the RequestStack
), after that it dispatches the event RequestEvent
. Some listeners will listen to this event and modify the request on the fly, to list out the listeners, we use this command in our terminal:
bin/console debug:event-dispatcher kernel.request
and see the result:
The most important listener at this stage is the RouterListener
, it will check the request URL to see if any controller is matched. If the request method
(e.g. GET, POST) is not matched or the URL is not matched, it will throw Exception, otherwise it will add the matched controller into the Request
.
Given that we have a simple controller
with show
action:
Simple controller with “show” action
Let’s check the RouterListener.php
:
RouterListener::onKernelRequest()
The matcher
will find the matched controller and return the result:
$parameters = [
"_route" => "post_show",
"_controller" => "App\Controller\PostController::show"
];
After this point, the RouterListener
already found the matched controller, concatenated the controller class
and controller action
into a string ( format class::action
), assigned to the _controller
key, and stored it in the request attributes
.
This is the main thing we should keep in mind about the request stage, that is finding the matched controller from the request URL, throw Exception if request method
or request URL
is not matched, otherwise store the controller information string (formatclass::method
) into the _controller
key in the request attributes
.
Load controller stage
“Load controller” stage
In this stage, the resolver
(ControllerResolver
) will parse the _controller
in the request attributes
into a callable array
contains a controller object and its method. It means the string PostController::show
will be “resolved” to become a callable
:
$controller = [(object) PostController, "show"]
After get callable array
and assigned to the $controller
, Symfony dispatch the ControllerEvent
, let’s see which services will listen by this command
bin/console debug:event-dispatcher kernel.controller
There are two important listeners here:
The ControllerListener
detects if the controller has a method __invoke()
, it means that client will call the controller without mention the method. In this case, the listener will add the method __invoke()
into the callable array
so that the array will behave the same with the normal controller.
ParamConverterListener
will check if it can convert the controller action arguments. In our case, it see the we type-hint the Post
entity in our controller arguments, so it uses the built-in DoctrineParamConverter
to get the Post
object having id
in the request URL. This converter helps our controller thin because we will not need to inject the PostRepository
into the controller to find the Post
object.
Controller arguments stage
“Controller arguments” stage
In this stage, the argumentResolver
(ArgumentResolver)
will assign a suitable value to the controller action arguments.
There are some built-in ArgumentResolver located in vendor/symfony/http-kernel/Controller/ArgumentResolver
, among them, three following resolvers are usually used: RequestValueResolver
, ServiceValueResolver
, RequestAttributeValueResolver
. They all help in our PostController
:
RequestValueResolver
helps getting theRequest
object fromRequest
type-hintServiceValueResolver
helps getting theReview
service from theReview
service type-hintRequestAttributeValueResolver
helps gettinghasComments
value from the request URL wild card
Response stage
“Response” stage
It’s time to call the controller method with the resolved arguments from the previous “controller arguments stage” to get response
.
In this stage, Symfony conduct an additional check to see if our controller action returns a Response
, then it will dispatch the ResponseEvent
and the FinishRequestEvent
. In this stage, the Request
is also popped out the RequestStack
.
As always, we use the commands to see which listeners to these events:
bin/console debug:event-dispatcher kernel.response
bin/console debug:event-dispatcher kernel.finish_request
The HttpKernel\..\ResponseListener
will have some adjustments in the headers such as Content-Type
, Content-Length
, fix the protocol version
.
The RememberMe\ResponseListener
will add remember me cookie
to theResponse
if some services created it in the Request attributes
.
That is the general Symfony Request & Response flow.