Create your own framework... on top of the Symfony2 Components (part 12)

Fabien Potencier

January 25, 2012

This article is part of a series of articles that explains how to create a framework with the Symfony2 Components: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.

In the last installment of this series, we have emptied the Simplex\\Framework class by extending the HttpKernel class from Symfony. Seeing this empty class, you might be tempted to move some code from the front controller to it:

<?php
 
// example.com/src/Simplex/Framework.php
 
namespace Simplex;
 
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
 
class Framework extends HttpKernel\HttpKernel
{
    public function __construct($routes)
    {
        $context = new Routing\RequestContext();
        $matcher = new Routing\Matcher\UrlMatcher($routes, $context);
        $resolver = new HttpKernel\Controller\ControllerResolver();
 
        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
 
        parent::__construct($dispatcher, $resolver);
    }
}
 

The front controller code would become more concise:

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../vendor/.composer/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
 
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$framework = new Simplex\Framework($routes);
 
$framework->handle($request)->send();
 

Having a more concise front controller means that you can have more than one for a single application. Why would it be useful? To allow having different configuration for the development environment and the production one for instance. In the development environment, you might want to have error reporting turned on and errors displayed in the browser to ease debugging:

ini_set('display_errors', 1);
error_reporting(-1);
 

... but you certainly won't want that same configuration on the production environment. Having two different front controllers gives you the opportunity to have a slightly different configuration for each of them.

So, moving code from the front controller to the framework class makes our framework more configurable, but at the same time, it introduces a lot of issues:

  • We are not able to register custom listeners anymore as the dispatcher is not available outside the Framework class (an easy workaround could be the adding of a Framework::getEventDispatcher() method);

  • We have lost the flexibility we had before; you cannot change the implementation of the UrlMatcher or of the ControllerResolver anymore;

  • Related to the previous point, we cannot test our framework easily anymore as it's impossible to mock internal objects;

  • We cannot change the charset passed to ResponseListener anymore (a workaround could be to pass it as a constructor argument).

The previous code did not exhibit the same issues because we used dependency injection; all dependencies of our objects were injected into their constructors (for instance, the event dispatcher were injected into the framework so that we had total control of its creation and configuration).

Does it means that we have to make a choice between flexibility, customization, ease of testing and not having to copy and paste the same code into each application front controller? As you might expect, there is a solution. We can solve all these issues and some more by using the Symfony2 dependency injection container:

{
    "require": {
        "symfony/class-loader": "2.1.*",
        "symfony/http-foundation": "2.1.*",
        "symfony/routing": "2.1.*",
        "symfony/http-kernel": "2.1.*",
        "symfony/event-dispatcher": "2.1.*",
        "symfony/dependency-injection": "2.1.*"
    },
    "autoload": {
        "psr-0": { "Simplex": "src/", "Calendar": "src/" }
    }
}
 

Create a new file to host the dependency injection container configuration:

<?php
 
// example.com/src/container.php
 
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
 
$sc = new DependencyInjection\ContainerBuilder();
$sc->register('context', 'Symfony\Component\Routing\RequestContext');
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
    ->setArguments(array($routes, new Reference('context')))
;
$sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');
 
$sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
    ->setArguments(array(new Reference('matcher')))
;
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
    ->setArguments(array('UTF-8'))
;
$sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener')
    ->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction'))
;
$sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher')
    ->addMethodCall('addSubscriber', array(new Reference('listener.router')))
    ->addMethodCall('addSubscriber', array(new Reference('listener.response')))
    ->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
;
$sc->register('framework', 'Simplex\Framework')
    ->setArguments(array(new Reference('dispatcher'), new Reference('resolver')))
;
 
return $sc;
 

The goal of this file is to configure your objects and their dependencies. Nothing is instantiated during this configuration step. This is purely a static description of the objects you need to manipulate and how to create them. Objects will be created on-demand when you access them from the container or when the container needs them to create other objects.

For instance, to create the router listener, we tell Symfony that its class name is Symfony\Component\HttpKernel\EventListener\RouterListeners, and that its constructor takes a matcher object (new Reference('matcher')). As you can see, each object is referenced by a name, a string that uniquely identifies each object. The name allows us to get an object and to reference it in other object definitions.

By default, every time you get an object from the container, it returns the exact same instance. That's because a container manages your "global" objects.

The front controller is now only about wiring everything together:

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../vendor/.composer/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
 
$routes = include __DIR__.'/../src/app.php';
$sc = include __DIR__.'/../src/container.php';
 
$request = Request::createFromGlobals();
 
$response = $sc->get('framework')->handle($request);
 
$response->send();
 

As all the objects are now created in the dependency injection container, the framework code should be the previous simple version:

<?php
 
// example.com/src/Simplex/Framework.php
 
namespace Simplex;
 
use Symfony\Component\HttpKernel\HttpKernel;
 
class Framework extends HttpKernel
{
}
 

If you want a light alternative for your container, consider Pimple, a simple dependency injection container in about 60 lines of PHP code.

Now, here is how you can register a custom listener in the front controller:

$sc->register('listener.string_response', 'Simplex\StringResponseListener');
$sc->getDefinition('dispatcher')
    ->addMethodCall('addSubscriber', array(new Reference('listener.string_response')))
;
 

Beside describing your objects, the dependency injection container can also be configured via parameters. Let's create one that defines if we are in debug mode or not:

$sc->setParameter('debug', true);
 
echo $sc->getParameter('debug');
 

These parameters can be used when defining object definitions. Let's make the charset configurable:

$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
    ->setArguments(array('%charset%'))
;
 

After this change, you must set the charset before using the response listener object:

$sc->setParameter('charset', 'UTF-8');
 

Instead of relying on the convention that the routes are defined by the $routes variables, let's use a parameter again:

$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
    ->setArguments(array('%routes%', new Reference('context')))
;
 

And the related change in the front controller:

$sc->setParameter('routes', include __DIR__.'/../src/app.php');
 

We have obviously barely scratched the surface of what you can do with the container: from class names as parameters, to overriding existing object definitions, from scope support to dumping a container to a plain PHP class, and much more. The Symfony dependency injection container is really powerful and is able to manage any kind of PHP classes.

Don't yell at me if you don't want to have a dependency injection container in your framework. If you don't like it, don't use it. It's your framework, not mine.

This is (already) the last part of my series on creating a framework on top of the Symfony2 components. I'm aware that many topics have not been covered in great details, but hopefully it gives you enough information to get started on your own and to better understand how the Symfony2 framework works internally.

If you want to learn more, I highly recommend you to read the source code of the Silex micro-framework, and especially its Application class.

Have fun!

~~ FIN ~~

P.S.: If there is enough interest (leave a comment on this post), I might write some more articles on specific topics (using a configuration file for routing, using HttpKernel debugging tools, using the build-in client to simulate a browser are some of the topics that come to my mind for instance).

Discussion

gravatar Karma Dordrak  — January 25, 2012 07:51   #1
Fabien,

Absolutely, if you find some time you should keep going. I like the way you introduce concepts (like the whole observer pattern) by stealth as you introduce the topics. It's excellent to raise the bar among PHP programmers and also great documentation for Symfony2 components. My only other suggestion would be to change the titles to reflect the discussed topics - good for SEO, bookmarks and easy reference in the future as currently you don't know what you are getting until you read into the article.

Kudos to you.
gravatar Larry Garfield  — January 25, 2012 08:02   #2
What Karma said. Links from each article to an index of all the articles would be good, too. That way I have a single page to point everyone I know at. :-)

If you do have time to continue it, sub-requests are, I think, the next topic to get to. If you're doing anything with a complex configurable page rather than trivial "every page is its own template and you're done" pages, then being able to layer controllers seems like an important next step.

Thanks again, Fabien!
gravatar Larry Garfield  — January 25, 2012 08:03   #3
And as I was posting that last comment, it looks like you added cross-links to previous articles. Now that's what I call responsive web pages!
gravatar Marc Torres Baix  — January 25, 2012 08:15   #4
Hi,

first of all a BIG THANKS for taking your time to write these series of articles, it has been very exciting and fun to play with them.

If you've more time to dedicate to it, my vote goes for:
+1 on Http debugging tools
+1 on the built-in client (I guess you mean Browserkit + CSSSelector components)

Salut!

PS: BTW you've a little typo naming the RouteListener Class, you've typed RouteListeners (notice the last 's') when describing the container definition.
gravatar Pierre Minnieur  — January 25, 2012 09:16   #5
Thank you for this series of articles Fabien!
gravatar Christian Schaefer  — January 25, 2012 09:32   #6
I have to say I am very impressed by this series. You managed to introduce a lot of concepts in easily digestible parts all based on solvin real world problems. I get the feeling that in terms of introduction and getting into this series is by far the best Symfony documentation there is (while the official one is a good reference documentation of course).

There are no particular parts I would like you to focus on but I would love to see you keep going. :)

Thanks a lot!
gravatar Ruben Gonzalez  — January 25, 2012 10:06   #7
I love this series. Thank you Fabien.
gravatar Loïc Chardonnet  — January 25, 2012 11:58   #8
Excellent series, which I will recommend to every PHP developer I know :) .

Looking at the Symfony2's components, I think some parts on Templating/CssSelector, Locale/Transaltion, Form/Validation and Security would be great :) .

Anyway, these 12 parts would be entirely satisfiying if you keep your mind on stopping here.
Thank you very much!

gravatar Raine Nguyen  — January 25, 2012 13:19   #9
Im getting Catchable fatal error: Argument 1 passed to Symfony\Component\Routing\Matcher\UrlMatcher::__construct() must be an instance of Symfony\Component\Routing\RouteCollection, instance of Symfony\Component\HttpKernel\EventListener\RouterListener given

Are you sure this line is correct:

$sc->register('framework', 'Simplex\Framework')
->setArguments(array(new Reference('dispatcher'), new Reference('resolver')))
gravatar Andreas Kleemann  — January 25, 2012 15:30   #10
Thank you, Fabien, for that great series of simple explanations, why Symfony2 Components and Symfony2 as a framwork has so much power in so many ways!
gravatar Pablo Godel  — January 25, 2012 15:56   #11
Thanks Fabien for the series of articles. This could very well go into a book due to the high quality. And it should become an introductory guide to the Symfony2 Components.

Another series could be created to write CLI apps with the Console, Process, Filesystem and Finder components.

For web, the Security component and sub-requests have my vote.
gravatar Mark Nielsen  — January 25, 2012 18:02   #12
+1 for more articles. These are very interesting and provide a great level of detail. Sometimes I look at a Symfony2 component and I don't realize the use cases it is meant to solve. These articles and the new component docs help to solve that. Cheers!
gravatar Johnny Peck  — January 25, 2012 21:05   #13
More articles would be amazing. Your writing style and way of introducing and simplifying complex topics is very appealing.

+1 for Security and/or Form components up next!
gravatar Balázs Benyó  — January 25, 2012 21:43   #14
Great articles. Keep going Fabien!
gravatar Raine Nguyen  — January 27, 2012 02:45   #15
Is anyone else putting this in real code to try? Because Im getting the error I posted above plus:

use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;

--> Fatal error: Cannot use Symfony\Component\HttpKernel as HttpKernel because the name is already in use in
gravatar Benjamin RICHARD  — January 27, 2012 11:21   #16
Your articles was really interesting, thanks for that!
gravatar Cédric Floquet  — January 27, 2012 14:51   #17
Fabien, thanks for this great tutorial. It's very informative and useful.

I hadn't used Symfony until now, and this is really useful to learn more about it, and practice.

@Raine Nguyen:
I had the same problem as you, part 12 is not very clear about some things.

Actually, as soon as you have created and are using the container configuration, your Framework class should be reverted to the way it was at the beginning of part 11, i.e. a simple child class of HttpKernel, without additionnal code.

If your class looks like the first code block of part 11, both of your problems go away.

This is because in the container config, we define Framework to accept a dispatcher and a resolver as parameters, just like HttpKernel does.

All objects the resolver and dispatcher depend upon will also be created by the container whenever the Framework object is created, since these dependencies are defined in the container, too.

Also, if you went all the way in part 11 and made your LeapYearController return strings instead of Response objects, for the sake of the exercise (creating a pluggable StringResponseListener), you should either change back your controller to return response objects again (that's what I did)... or register your StringResponseListener in the container. Otherwise you'll get an error about the responses returned by the controller not being Response objects.

Hope this helps, and my english is not too confusing :)
gravatar Yader Hernandez  — January 30, 2012 01:40   #18
I'm against on the "Create a new file to host the dependency injection container configuration" advice.

Instead of having a file return DependencyInjection\ContainerBuilder I'd much rather use/see a class that can be instantiated/called statically. Mainly for 2 reasons:

1. I can unit test it
2. I don't have to do a nasty include of the file, instead I can instantiate an object that returns me what I need. For example:

use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;

class SomeGenericUtilClass {

public static function buildContainer(ContainerBuilder $sc) {
$sc->register('context', 'Symfony\Component\Routing\RequestContext');
$sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher')
->setArguments(array($routes, new Reference('context')))
;
$sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver');

$sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener')
->setArguments(array(new Reference('matcher')))
;
$sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener')
->setArguments(array('UTF-8'))
;
$sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener')
->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction'))
;
$sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher')
->addMethodCall('addSubscriber', array(new Reference('listener.router')))
->addMethodCall('addSubscriber', array(new Reference('listener.response')))
->addMethodCall('addSubscriber', array(new Reference('listener.exception')))
;
$sc->register('framework', 'Simplex\Framework')
->setArguments(array(new Reference('dispatcher'), new Reference('resolver')))
;

return $sc;
}
}

Then at the bootstrapping level, all I would have to do is:

$builder = SomeGenericUtilClass::buildContainer(new DependencyInjection\ContainerBuilder();)
gravatar Raine Nguyen  — January 30, 2012 12:00   #19
@Cédric Floquet: Thank you so much, it works now

Regards
gravatar Fran Diéguez  — February 01, 2012 15:53   #20
The problem that I found now is that I can't access the service container from the controllers.

For example for get the logger, mailer, database connection, ...
gravatar Lukas Kahwe Smith  — February 01, 2012 18:41   #21
The topic if injecting the DIC into the controller is a big debate. Symfony2 and ZF2 by default (or lets rather say that is how most examples are written) do inject the DIC into the Controller.

I personally think this is a horrible idea. Injecting the DIC should be done as rarely as possible. There are a few edge cases where its necessary.

See also:
https://github.com/liip/LiipContainerWrapperBundle
http://miller.limethinking.co.uk/2011/05/19/when-dependency-injection-goes-wrong/
gravatar Ivan Rey  — February 02, 2012 02:18   #22
Great series!

+1 for more chapters, any topic is good, but I'd like to see more of using a configuration file for routing and DIC.

gravatar Paul Gamper  — March 01, 2012 14:46   #23
Please write about templates :).
gravatar Duilio Palacios  — March 12, 2012 16:03   #24
When I started reading these articles, I already knew that:

1. I was going to use the complete Symfony2 framework anyway.
2. The last article was going to be about the DIC pattern.

Anyway, I learned a lot :) That's more or less how I created the bootstrap of my "home made framework" a long ago.
gravatar Andrey Esaulov  — March 13, 2012 11:43   #25
Great series! One of the best thing about Symfony was it's documentation. I literally learned object-oriented programming by reading the famous jobeet-tutorial. And now the Symfony2 Components takes my knowledge even further.
To give something back to the community I've decided to put together the series of video tutorials, or so called screencasts. They are using the material from this series, but do it in the visual fassion.
So if you are a visual learner, you are welcome to take a look at these tutorials on my site:

http://object-oriented-php.com/