Symfony Service Container: The Need for Speed

Fabien Potencier

April 02, 2009

This article is part of a series on Dependency Injection in general and on a lightweight implementation of a Container in PHP in particular:

During the first five articles of this series on Dependency Injection, we have progressively introduced the main concepts behind this simple and useful design pattern. We have also talked about the implementation of a lightweight PHP container that will be used for Symfony 2.

But with the introduction of the XML and YAML configuration files, you might have became a bit sceptic about the performance of the container itself. Even if services are lazy loading, reading a bunch of XML or YAML files on each request and creating objects by using introspection is probably not very efficient in PHP. And because the container is almost always the corner stone of any application using it, its speed does matter a lot.

On the one hand, using XML or YAML to describe services and their configuration is very powerful and flexible:

<container xmlns="http://symfony-project.org/2.0/container">
  <parameters>
    <parameter key="mailer.username">foo</parameter>
    <parameter key="mailer.password">bar</parameter>
    <parameter key="mailer.class">Zend_Mail</parameter>
  </parameters>
  <services>
    <service id="mail.transport" class="Zend_Mail_Transport_Smtp" shared="false">
      <argument>smtp.gmail.com</argument>
      <argument type="collection">
        <argument key="auth">login</argument>
        <argument key="username">%mailer.username%</argument>
        <argument key="password">%mailer.password%</argument>
        <argument key="ssl">ssl</argument>
        <argument key="port">true</argument>
      </argument>
    </service>
    <service id="mailer" class="%mailer.class%">
      <call method="setDefaultTransport">
        <argument type="service" id="mail.transport" />
      </call>
    </service>
  </services>
</container>
 

But, on the other hand, defining the service container as a plain PHP class gives you the full speed, as seen during the second article of this series:

class Container extends sfServiceContainer
{
  static protected $shared = array();
 
  protected function getMailTransportService()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth'     => 'login',
      'username' => $this['mailer.username'],
      'password' => $this['mailer.password'],
      'ssl'      => 'ssl',
      'port'     => 465,
    ));
  }
 
  protected function getMailerService()
  {
    if (isset(self::$shared['mailer']))
    {
      return self::$shared['mailer'];
    }
 
    $class = $this['mailer.class'];
 
    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransportService());
 
    return self::$shared['mailer'] = $mailer;
  }
}
 

The above code does the bare minimum to provide flexibility thanks to the configuration variables, and still be very fast.

How can you have the best of both world? That's quite simply. The Symfony Dependency Injection component provides yet another built-in dumper: a PHP dumper. This dumper can convert any service container to plain PHP code. That's right, it is able to generate the code you could have written by hand in the first place.

Let's use again our Zend_Mail example and for brevity's sake, let's use the XML definition file created in the previous article:

$sc = new sfServiceContainerBuilder();
 
$loader = new sfServiceContainerLoaderFileXml($sc);
$loader->load('/somewhere/container.xml');
 
$dumper = new sfServiceContainerDumperPhp($sc);
 
$code = $dumper->dump(array('class' => 'Container'));
 
file_put_contents('/somewhere/container.php', $code);
 

As for any other dumper, the sfServiceContainerDumperPhp class takes a container as its constructor first argument. The dump() method takes an array of options, and one of them is the name of the class to generate.

Here is the generated code:

class Container extends sfServiceContainer
{
  protected $shared = array();
 
  public function __construct()
  {
    parent::__construct($this->getDefaultParameters());
  }
 
  protected function getMailTransportService()
  {
    $instance = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth' => 'login',
      'username' => $this->getParameter('mailer.username'),
      'password' => $this->getParameter('mailer.password'),
      'ssl' => 'ssl',
      'port' => 465
    ));
 
    return $instance;
  }
 
  protected function getMailerService()
  {
    if (isset($this->shared['mailer'])) return $this->shared['mailer'];
 
    $class = $this->getParameter('mailer.class');
    $instance = new $class();
    $instance->setDefaultTransport($this->getMailTransportService());
 
    return $this->shared['mailer'] = $instance;
  }
 
  protected function getDefaultParameters()
  {
    return array (
      'mailer.username' => 'foo',
      'mailer.password' => 'bar',
      'mailer.class' => 'Zend_Mail',
    );
  }
}
 

If you have a closer look at the code generated by the dumper, you will notice that the code is very similar to the one we wrote by hand.

The generated code does not use the shortcut notation to access parameters and services to be as fast as possible.

By using the sfServiceContainerDumperPhp dumper, you can have the best of both world: the flexibility of the XML or YAML format to describe and configure your services, and the speed of an optimized and auto-generated PHP file.

Of course, as projects have almost always different settings for different environments, you can of course generate different container classes, based on the environment or on a debugging setting. Here is a small snippet of PHP code that illustrates how to build the container dynamically for the very first request, and use a cached one for all other requests when not in debugging mode:

$name = 'Project'.md5($appDir.$isDebug.$environment).'ServiceContainer';
$file = sys_get_temp_dir().'/'.$name.'.php';
 
if (!$isDebug && file_exists($file))
{
  require_once $file;
  $sc = new $name();
}
else
{
  // build the service container dynamically
  $sc = new sfServiceContainerBuilder();
  $loader = new sfServiceContainerLoaderFileXml($sc);
  $loader->load('/somewhere/container.xml');
 
  if (!$isDebug)
  {
    $dumper = new sfServiceContainerDumperPhp($sc);
 
    file_put_contents($file, $dumper->dump(array('class' => $name));
  }
}
 

That wraps up our tour of the Symfony 2 Dependency Injection Container.

Before closing this series, I want to show you yet another great feature of the dumpers. Dumpers can do a lot of different things, and to demonstrate the decoupling of the implementation of the component, I have implemented a Graphviz dumper. What for? To help you visualize your services and their dependencies.

First, let's see how to use it on our example container:

$dumper = new sfServiceContainerDumperGraphviz($sc);
file_put_contents('/somewhere/container.dot', $dumper->dump());
 

The Graphviz dumper generates a dot representation of your container:

digraph sc {
  ratio="compress"
  node [fontsize="11" fontname="Myriad" shape="record"];
  edge [fontsize="9" fontname="Myriad" color="grey" arrowhead="open" arrowsize="0.5"];
 
  node_service_container [label="service_container\nsfServiceContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"];
  node_mail_transport [label="mail.transport\nZend_Mail_Transport_Smtp\n", shape=record, fillcolor="#eeeeee", style="dotted"];
  node_mailer [label="mailer\nZend_Mail\n", shape=record, fillcolor="#eeeeee", style="filled"];
  node_mailer -> node_mail_transport [label="setDefaultTransport()" style="dashed"];
}
 

This representation can be converted to an image by using the dot program:

$ dot -Tpng /somewhere/container.dot > /somewhere/container.png

Zend_Mail container PNG representation

For this simple example, the visualization has no real added value, but as soon as you start having more than a few services, it can be quite useful... and beautiful.

The Graphviz dumper dump() method takes a lot of different options to tweak the output of the graph. Have a look at the source code to discover the default values for each of them:

  • graph: The default options for the whole graph
  • node: The default options for nodes
  • edge: The default options for edges
  • node.instance: The default options for services that are defined directly by object instances
  • node.definition: The default options for services that are defined via service definition instances
  • node.missing: The default options for missing services

As a teaser for the next Symfony component that will be released later this month, here is the graph for an hypothetic CMS using the Symfony 2 new Templating Framework:

Symfony 2 Templating Framework

That's all for this series on Dependency Injection. I hope you have learned something reading these articles. I also hope you will try the Symfony 2 Service Container component soon and give me feedback about your usage. Also, if you create "recipes" for some existing Open-Source libraries, consider sharing them with the community. You can also send them to me and I will host them along side the container component to ease reusing.

Discussion

gravatar ace  — April 02, 2009 08:47   #1
Wouldn't be better idea to use standard solutions like Webservices for such matther?
gravatar Bernhard  — April 02, 2009 09:06   #2
Absolutely great! I didn't know that Graphviz can look so nice. I'll have to look into it.

Is the generated PHP code compatible with the original service container? That is, if I write
$service->mailer
and later switch to a generated code, does it work?

I think it is important that these two components have identical usage as to keep confusion beyond developers low when switching between the two implementations.
gravatar Fabien  — April 02, 2009 09:13   #3
@Bernhard: The generated code is exactly the code you would have written by hand. So, yes, you can switch from a generated container, to a hand-coded one, or a builded one with XML or YAML without changing a single line in your code. That was one of the main goal for the container: start with a simple hardcoded container, move to an XML-described one, and compile it to PHP again for production. They all share the same interface.
gravatar Javi  — April 02, 2009 09:14   #4
According to new philosophy to "PDFed" anything, have you think about building a small doc with all that?

I am going to need to re-read everything quite slow to understand it properly, :-)
gravatar Michal  — April 02, 2009 09:36   #5
Really great articles. Do you plan to continue this for other symfony components? Like templating framework and symfony 2 core kernel. I really hope so.

I have couple of question how it will work in symfony 2:

Will those container classes be autogenerated (when file will be changed) from yml/xml or there will be task for it (like generating model)?

Also, will there will be one big container or many smaller containers?

How you will access container(s) object? By sfContext and proxy methods in sfComponent? Or some other way?
gravatar JPhilip  — April 02, 2009 18:16   #6
Does sfServiceContainerDumperPhp validate that the classes and methods or properties passed in the configuration really exist?
It seems it would be a good place to do that as well as base class validation...
PHP being an interpreted language, this step can bring some of the advantages of compiled languages like type checking only once before the program runs for the first time.
gravatar Fabien  — April 02, 2009 18:48   #7
@JPhilip: There is no validation, as for any other dumper, or when you create a container with the builder directly. We cannot really validate anything as classes are not always loaded and we just don't know how to load them.
gravatar Ian Dominey  — April 06, 2009 14:21   #8
@JPhilip: I believe that good functional testing (and trying to ensure near-as-damnit 100% code coverage) would be a good place to do this.
gravatar Matthias  — April 08, 2009 15:14   #9
Fabien, as I always study your code carefully, I wonder why didn't you postfix the files with .class.php but only .php?

Will this be the new style for Symfony 2.0 or the components?
gravatar Fabien  — April 08, 2009 15:34   #10
@Matthias: hehe, good catch! I used the .class.php suffix because symfony was my first PHP project, and Mojavi used this convention. As I now have a better understanding of PHP, I think .php as a suffix is much better. So, yes, this will be the new style for Symfony 2.0.
gravatar Matthias  — April 08, 2009 17:20   #11
I see. ;-)
I never understood why this suffix was used.. Now it's clear to me.

I also like adding 'Interface' at the end of interface names as this makes it easy for example in class type hints to distinguish between interfaces or classes.

I really appreciate the small things that matters! They make the difference. :-)
gravatar Alexis Metaireau  — April 23, 2009 10:15   #12
Thanks for your article serie about DI. Whereas DI is a simple concept, it's hard to explain, and you do it well.

I also work on a Di container component, for personal use and study, and I have a little bit different demarch: decoupling descriptions and containers.

I think Dumpers, for instance, had not to know how to build services.

Here's a link to the DI i'm working on. It's not finished anymore, but ideas are here: http://bitbucket.org/ametaireau/spiral/src/tip/core/di/

What do you think about it ?