Symfony Service Container: Using a Builder to create Services

Fabien Potencier

March 31, 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:

In the previous article on Dependency Injection, you learned how to use the sfServiceContainer class to provide a more appealing interface to your service containers. In this article, we will go one step further and learn how to leverage the sfServiceContainerBuilder class to describe services and their configuration in pure PHP code.

The Subversion repository has been updated with the code needed for this tutorial. If you have checkout the code yesterday, you can simple update it. If not, the repository is available at http://svn.symfony-project.com/components/dependency_injection/trunk/.

The sfServiceContainerBuilder class extends the basic sfServiceContainer class and allows the developer to describe services with a simple PHP interface.

The description of the services are done by registering service definitions. Each service definition describes a service: from the class to use to the arguments to pass to the constructor, and a bunch of other configuration properties (see the sfServiceDefinition sidebar below).

The Zend_Mail example can easily be rewritten by removing all the hardcoded code and building it dynamically with the builder class instead:

require_once 'PATH/TO/sf/lib/sfServiceContainerAutoloader.php';
sfServiceContainerAutoloader::register();
 
$sc = new sfServiceContainerBuilder();
 
$sc->
  register('mail.transport', 'Zend_Mail_Transport_Smtp')->
  addArgument('smtp.gmail.com')->
  addArgument(array(
    'auth'     => 'login',
    'username' => '%mailer.username%',
    'password' => '%mailer.password%',
    'ssl'      => 'ssl',
    'port'     => 465,
  ))->
  setShared(false)
;
 
$sc->
  register('mailer', '%mailer.class%')->
  addMethodCall('setDefaultTransport', array(new sfServiceReference('mail.transport')))
;
 

The creation of a service is done by calling the register() method, which takes the service name and the class name, and returns a sfServiceDefinition instance.

A service definition is internally represented by an object of class sfServiceDefinition. It is also possible to create one by hand and registering it directly by using the service container setServiceDefinition() method.

The definition object implements a fluid interface and provides methods that configure the service. In the above example, we have used the following ones:

  • addArgument(): Adds an argument to pass to the service constructor.

  • setShared(): Whether the service must be unique for a container or not (true by default).

  • addMethodCall(): A method to call after the service has been created. The second argument is an array of arguments to pass to the method.

Referencing a service is now done with a sfServiceReference instance. This special object is dynamically replaced with the actual service when the referencing service is created.

During the registration phase, no service is actually created, it is just about the description of the services. The services are only created when you actually want to work with them. It means you can register the services in any order without taking care of the dependencies between them. It also means you can override an existing service definition by re-registering a service with the same name. That's yet another simple way to override a service for testing purpose.

As the sfServiceContainerBuilder class implements the standard sfServiceContainerInterface interface, using the service container does not need to be changed:

$sc->addParameters(array(
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
  'mailer.class'    => 'Zend_Mail',
));
 
$mailer = $sc->mailer;
 

The sfServiceContainerBuilder is able to describe any object instantiation and configuration. We have demonstrated it with the Zend_Mail class, and here is another example using the sfUser class from Symfony:

$sc = new sfServiceContainerBuilder(array(
  'storage.class'        => 'sfMySQLSessionStorage',
  'storage.options'      => array('database' => 'session', 'db_table' => 'session'),
  'user.class'           => 'sfUser',
  'user.default_culture' => 'en',
));
 
$sc->register('dispatcher', 'sfEventDispatcher');
 
$sc->
  register('storage', '%storage.class%')->
  addArgument('%storage.options%')
;
 
$sc->
  register('user', '%user.class%')->
  addArgument(new sfServiceReference('dispatcher'))->
  addArgument(new sfServiceReference('storage'))->
  addArgument(array('default_culture' => '%user.default_culture%'))->
;
 
$user = $sc->user;
 

In the Symfony example, even if the storage object takes an array of options as an argument, we passed a string placeholder (addArgument('%storage.options%')). The container is smarter enough to actually pass an array, the value of the placeholder.

That's all for today. Using PHP code to describe the services is quite simple and powerful. It gives you a tool to create your container without duplicating too much code and to abstract objects instantiation and configuration. In the next article, we will see how services can also be described with XML or YAML files. Stay tuned!

Discussion

gravatar Alex  — March 31, 2009 10:37   #1
Cool!
Thanks a lot, but now I think it's time to see the class diagram (UML or something simplified).
Looking forward to read next articles.
gravatar david  — March 31, 2009 11:36   #2
Why do you have to call the name of the registered service, in the second listing mail.transport, as mail_transport?

Is it possible to use mail_transport as name? Or can the service reference be called as mail.transport?
gravatar Fabien  — March 31, 2009 11:40   #3
@david: that was a typo ;) It is fixed now. Thanks.
gravatar david  — March 31, 2009 12:30   #4
I thought it was some functionality related to symphony :)

This serie really draws my attention to symphony 2.0 as a framework/components i could use for future webdevelopment. Nice work!
gravatar Ryan Weaver  — March 31, 2009 13:10   #5
This is excellent - I can see it coming together. Also - I think the Symfony Components idea is great - the independence of the Symfony components has been underutilized thus far.
gravatar Federico  — March 31, 2009 13:33   #6
Great way of adding extra value to the framework. This will definitely solve some of the problems I faced in the past. Well done.
gravatar David Herrmann  — March 31, 2009 14:13   #7
This looks very interesting. It still needs some time to settle and let the whole picture form itself, but I really like it!
gravatar Trevan Richins  — March 31, 2009 18:06   #8
So you either have to instantiate a sfServiceContainerBuilder every time you need a new object or pass an instance of the builder around? How does that help?
gravatar Les  — March 31, 2009 18:53   #9
This looks well smart... What I like about it in particular is the fact the dependencies are not created until you actually make use of a service.

When I have more time, going to take a look and see how it I can make use of it for my own framework.

Job well done folks ;)
gravatar Fabien  — April 01, 2009 07:20   #10
@Trevan Richins: The great thing about a DI container is that the managed objects do not know that they are managed by the container. You don't need to pass the container around as the objects themselves use DI. So their dependencies are injected. In the DI container world, some objects of course need to be "aware". But most of the time, it is only true for a small number of them. In a web framework for instance, it is perhaps true for the main Application object and perhaps the main controller. That's all.
gravatar Trevan Richins  — April 01, 2009 17:44   #11
So are you saying that basically all objects are created at the very beginning? So any leaf nodes of the dependency tree must be created at the beginning of the application so that the root nodes can be created and used? And what if those leaf nodes are not needed all the time, or if there is a ton of them (like model objects) and so you would normally loop through a subset of them in each iteration to make sure you don't run out of memory.
gravatar Fabien  — April 01, 2009 20:18   #12
@Trevan Richins: I am saying just the contrary. The objects are only created the first time they are needed. If an object needs to be created, its dependency objects, if any, are created, and so on recursively. So, if some objects are never used, or are not needed as a dependency, they are never created.
gravatar Ian Dominey  — April 06, 2009 14:04   #13
@Trevan Richins
I think I understand what you were talking about. In order to use DI throughout your application, you will need to either pass around an instance of the service container OR have the service container as a Singleton (shudder). I believe that the singleton approach is what Spring uses in Java (though I may be wrong on this point).

How does this help? Well, considering that the next article is all about using YAML and XML to configure your DI Container, you can use configuration files to control how your objects are constructed in different environments.

Correct me if I'm wrong on this point Fabien.
gravatar Jonathan  — April 15, 2009 18:35   #14
Fabien - I realize I'm late to the party here, but wanted to share my thoughts.

I'm a bit disappointed to see that registering services with the DI container is so verbose. Using other DI frameworks such as Guice, marking an injection point is as simple as placing an @Inject annotation on your constructor, field or method, and the framework uses reflection to obtain the member and parameter information. Obviously PHP doesn't support annotations natively, but I was still hoping for a simpler way of defining injection points.

Regarding constructor and method arguments, I would think that reflection could be used to prevent the user from having to manually add arguments. Is any of this workable?
gravatar Mike  — April 21, 2009 20:21   #15
I'm just starting to use the Zend framework so sorry for the noob question.

If I define Zend_Db as a service should I define it as shared or not.

I come from a Java+Spring background where you can set an object as a singleton (shared) within the context, provided the object's class is thread safe. I realize PHP is a different architecture than Java, but I'm not sure how or if I need to worry about concurrency issues related to a shared object.

Thanks.
gravatar Fabien  — April 22, 2009 08:49   #16
@Mike: "shared" means that the container will always return the same instance for your service. Of course, as the container itself is not a singleton, it does not mean that you cannot have another shared service within another container context. In PHP, there is no thread safe problem as everything is throw away between each request.
gravatar Lukas  — May 15, 2009 12:49   #17
I am also not sure how to deal with getting an instance of the service container. Fowler, does not seem to offer an answer, actually he seems to be not so fond of the service container idea:
http://www.martinfowler.com/articles/injection.html#DecidingWhichOptionToUse

I do not like the static calls to the service locator in his code examples and as a result he says its your choice if you can live with the dependency on this single service container class being referenced in the code. Obviously in a dynamic language and given the configurability of Fabien solution, it might be ok to go with that route:
public function __construct($db = null) {
if (is_null($db)) {
$db = servicecontainer::get('db');
}
$this->db = $db;
}

Note that the dumper Fabien developed does not produce static methods.

Simply also injecting an instance of the service container as sort of a must have default parameter is one approach one could take:
public function __construct($sc, $db = null) {
if (is_null($db)) {
$db = $sc->db;
}
$this->db = $db;
}

But then one in theory also needs to store an instance of the service container in a property in case that class needs to construct an instance of a class. This gets you circular references, that are very nasty (though PHP 5.3 can at least clean those up memory-wise if necessary, albeit with some overhead).
gravatar Lukas  — May 20, 2009 11:05   #18
Just FYI: a discussion ensued on my blog about my questions:
http://pooteeweet.org/blog/1480