Symfony2: the big picture
Behind the scenes of a well-thought framework
I started playing with Symfony2 when it was in beta. The biggest obstacle in my learning process was the lack of an architectural overview; something that would describe the flow of execution of every request, at least at a very coarse grain. I tried to read the official docs and various articles on the Net: I was able to do things but didn’t know how (and why) they worked – when they did. The immediate consequence was that I was almost always unable to adapt the examples to my specific needs.
Fabien to the rescue
Then I read Fabien Potencier’s series “Create your own framework… on top of the Symfony2 Components” and began to understand. Symfony2 architecture is based on two key subsystems: Dependency Injection Container (DIC) and Event Dispatcher. Knowing these two, everything else was easy to learn for me. I’ll share this knowledge, hoping it will help someone else to learn Symfony2.
Execution flow
Every web request (larger picture) is processed by web/app.php. It initializes the autoloader and instantiates an AppKernel that will process the request. A Request object is created from globals ($_GET, $_POST, etc) and passed to the AppKernel via the handle() method. AppKernel, in turn, calls the handle() method in HttpKernel and the real processing begins. The handle() method creates an Event object containing the original Request object and some other bits. It then calls the dispatch() method of the EventDispatcher, passing the Event and a constant (KernelEvents::REQUEST) to specify the event type. The EventDispatcher has a list of listeners that are called in sequence passing the Event to each of them. The EventDispatcher knows which listeners to call, because each listener registers itself in the EventDispatcher, indicating the event type they are interested in.
The net result is that the original request get processed by a series of listeners that can modify the request object.
The same procedure is then repeated for KernelEvents::CONTROLLER events, KernelEvents::VIEW events and KernelEvents::RESPONSE events. In the last round, the Event carries a Response object that is returned back to web/app.php. The Request object is then sent to the net via its send() method.
The actual flow in HttpKernel::handle() is a bit more complicated, but the basic idea doesn’t change.
Let the magic begin
The first round of listeners act on an Event containing the Request object. For example, routing is done in this phase. A RouterListener parses the request and adds specific fields to indicate the selected route and any additional route parameters. This information is used by a ControllerResolver to determine the correct Controller and action to call. Also the Firewall is called in this round to check if access control should be enforced.
At the end of the KernelEvents::REQUEST dispatching, the HttpKernel checks if the event contains a Response. If it does, it skips the other phases and immediately returns the Response to web/app.php (it’s a slight simplification but the concept remains). We could use this mechanism to write a simple page cache. We could write a listener for KernelEvents::REQUEST. When it gets called, it examines the URI, checks if the corresponding page is in the cache, and eventually returns it back in a Response. The listener can then instruct the EventDispatcher to stop calling other listeners, using the Event::stopPropagation() method. With this trick you can serve a page very quickly, without any further processing (controllers, databases, etc.)
The magic cook
The other architectural cornerstone in Symfony2 is the Dependency Injection Container, DIC for brevity. Complete documentation
for the DIC is at the Symfony website. Basically, the DIC is a recipe
book for constructing objects and maintaining a reference to them. Every
time you ask for a service (an object), the DIC creates the object
using your recipes and returns it. If it was already created, it returns
a reference to the existing object. Why should you use it? There are a
few presentations on Slideshare from Fabien, with a thorough discussion.
It gives you the consistency of a registry, without resorting to
globals, singletons and other tricks that render your code untestable.
It is so simple and powerful that the entire framework is built using
the DIC.
Wrapping up
Your request enters the kernel and is passed to various listeners. If
you don’t need special processing, you can generate a response in the
controller; it is called from the HttpKernel::handle() method after the KernelEvents::CONTROLLER event.
If you need special processing before the controller is called (custom
authentication, logging, etc.), you can add a listener to the
KernelEvents::REQUEST event, and perform your processing inside the
listener. The listener can be easily added using the DIC, and it can
access all services managed by the DIC: logger, Doctrine, router,
mailer, HttpKernel, etc.
If you need special processing after the controller is called
(filtering, logging, compression, etc.), you can add a listener to the
KernelEvents::RESPONSE event, and perform your custom processing inside
the listener.
Final note
This guide is work in progress. Any suggestion, criticism, fixes and contribution are highly appreciated.