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:
- Part 1: What is Dependency Injection?
- Part 2: Do you need a Dependency Injection Container?
- Part 3: Introduction to the Symfony Service Container
- Part 4: Symfony Service Container: Using a Builder to create Services
- Part 5: Symfony Service Container: Using XML or YAML to describe Services
- Part 6: The Need for Speed
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 Service Container Interface
All service container classes share the same interface, defined in
sfServiceContainerInterface:interface sfServiceContainerInterface { public function setParameters(array $parameters); public function addParameters(array $parameters); public function getParameters(); public function getParameter($name); public function setParameter($name, $value); public function hasParameter($name); public function setService($id, $service); public function getService($id); public function hasService($name); }
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 containersetServiceDefinition()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 (trueby 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.
The
sfServiceDefinitionClassA service has several properties that changes the way it is created and configured:
setConstructor(): Sets the static method to use when the service is created, instead of the standardnewconstruct (useful for factories).
setClass(): Sets the service class.
setArguments(): Sets the arguments to pass to the constructor (the order is of course significant).
addArgument(): Adds an argument for the constructor.
setMethodCalls(): Sets the service methods to call after service creation. These methods are called in the same order as the registration.
addMethodCall(): Adds a service method call to call after service creation. You can add a call to the same method several times if needed.
setFile(): Sets a file to include before creating a service (useful if the service class if not autoloaded).
setShared(): Whether the service must be unique for a container or not (trueby default).
setConfigurator(): Sets a PHP callable to call after the service has been configured.
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
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.
Is it possible to use mail_transport as name? Or can the service reference be called as mail.transport?
This serie really draws my attention to symphony 2.0 as a framework/components i could use for future webdevelopment. Nice work!
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 ;)
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.
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?
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.
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).
http://pooteeweet.org/blog/1480