Do you need a Dependency Injection Container?

Fabien Potencier

March 28, 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 first installment of this series on Dependency Injection, I have tried to give concrete web examples of Dependency Injection in action. Today, I will talk about Dependency Injection Containers.

First, let's start with a bold statement:

Most of the time, you don't need a Dependency Injection Container to benefit from Dependency Injection.

But when you need to manage a lot of different objects with a lot of dependencies, a Dependency Injection Container can be really helpful (think of a framework for instance).

If you remember the example of the first article, creating a User object required to first create a SessionStorage object. Not a big deal, but still, you have to know about all the dependencies you need before creating the object you need:

$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);
 

In the upcoming articles, we will talk about the PHP implementation of a Dependency Injection Container for Symfony 2. As I want to make it clear that the implementation is in no way bound to Symfony, I will take Zend Framework examples to illustrate my articles.

Contrary to popular belief, there is no such thing as a PHP framework war. I really appreciate the Zend Framework components, and as a matter of fact, a lot of their libraries can be really useful in a Symfony project.

The Zend Framework Mail library, which ease emails management, uses the PHP mail() function by default to send emails, which is not really flexible. Thankfully, it is quite easy to change this behavior by providing a transport object. The following snippet of code shows how to create a Zend_Mail object that sends its emails using a Gmail account:

$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
  'auth'     => 'login',
  'username' => 'foo',
  'password' => 'bar',
  'ssl'      => 'ssl',
  'port'     => 465,
));
 
$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);
 

To keep this article short enough, I use simple examples. Of course, for these simple examples, it does not make sense to have a container. Think of the examples as being just a small part of the collections of objects that need to be managed by the container.

A Dependency Injection Container is an object that knows how to instantiate and configure objects. And to be able to do its job, it needs to knows about the constructor arguments and the relationships between the objects.

Here is a simple hardcoded container for the above Zend_Mail example:

class Container
{
  public function getMailTransport()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth'     => 'login',
      'username' => 'foo',
      'password' => 'bar',
      'ssl'      => 'ssl',
      'port'     => 465,
    ));
  }
 
  public function getMailer()
  {
    $mailer = new Zend_Mail();
    $mailer->setDefaultTransport($this->getMailTransport());
 
    return $mailer;
  }
}
 

Using the container class is simple enough:

$container = new Container();
$mailer = $container->getMailer();
 

When using the container, we just ask for a mailer object, and we don't need to know anything about how to create it anymore; all the knowledge about how to create an instance of the mailer is now embedded into the container. The mail transport dependency will be injected automatically by the container, thanks to the getMailTransport() call. All the power of the container lies in this simple call!

But, astute readers might have noticed a problem here. The container itself has everything hardcoded! So, we need to go one step further and add parameters to the mix to make the container really useful:

class Container
{
  protected $parameters = array();
 
  public function __construct(array $parameters = array())
  {
    $this->parameters = $parameters;
  }
 
  public function getMailTransport()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', array(
      'auth'     => 'login',
      'username' => $this->parameters['mailer.username'],
      'password' => $this->parameters['mailer.password'],
      'ssl'      => 'ssl',
      'port'     => 465,
    ));
  }
 
  public function getMailer()
  {
    $mailer = new Zend_Mail();
    $mailer->setDefaultTransport($this->getMailTransport());
 
    return $mailer;
  }
}
 

It is now easy to change the Google username and password by passing some parameters to the container constructor:

$container = new Container(array(
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
));
$mailer = $container->getMailer();
 

If you need to change the mailer class for testing, the object class name can also be passed as a parameter:

class Container
{
  // ...
 
  public function getMailer()
  {
    $class = $this->parameters['mailer.class'];
 
    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransport());
 
    return $mailer;
  }
}
 
$container = new Container(array(
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
  'mailer.class'    => 'Zend_Mail',
));
$mailer = $container->getMailer();
 

Last, but not the least, each time I want to get a mailer, I don't need a new instance of it. So, the container can be changed to always return the same object:

class Container
{
  static protected $shared = array();
 
  // ...
 
  public function getMailer()
  {
    if (isset(self::$shared['mailer']))
    {
      return self::$shared['mailer'];
    }
 
    $class = $this->parameters['mailer.class'];
 
    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransport());
 
    return self::$shared['mailer'] = $mailer;
  }
}
 

With the introduction of the static $shared property, each time you call the getMailer() method, the object created for the first call will be returned.

That wraps up the basic features that need to be implemented by a Dependency Injection Container. A Dependency Injection Container manages objects: from their instantiation to their configuration. The objects themselves do not know that they are managed by a container and know nothing about the container. That's why a container is able to manage any PHP object. It is even better if the objects use dependency injection for their dependencies, but that's not a prerequisite.

Of course, creating and maintaining the container class by hand can become a nightmare pretty fast. But as the requirements are quite minimal for a container to be useful, it is easy to implement one. The next installment of this series will talk about the Symfony 2 dependency injection container implementation.

Discussion

spacer Ad  — March 28, 2009 10:06   #1
Im getting the big picture now. These are some awsome articles Fabien. To the point and really show the usefullness (power) of dependency injectors.

Keep it up! in symfony we trust ;)
spacer mazenovi  — March 28, 2009 10:15   #2
great those articles on design pattern, very clear!
Just two questions:
- As a Dependency Injection is manipulating objects which have potentially similar structures, why don't use the notion of interface?
- For the unique instance of an object what is the difference between this technic and singleton?

Thanks for your posts
spacer kasn  — March 28, 2009 10:30   #3
I'm not sure if this is real Dependency Injection, or just some strange mix of a factory and a singleton.

How do you test the code? There is no way to mock the transport for testing purpose and in my world, thats what DI is about, making code easy to mock and to test.
spacer Fabien  — March 28, 2009 11:17   #4
@mazenovi: A singleton is when you can only have one instance of an object. Moreover, the singleton pattern is implemented by the class itself by protecting the constructor and providing a static method to access the only available instance. For the mail object, things are totally different. The Zend_Mail class is not a singleton and you have a different instance per container. As the container itself is not a singleton, the objects contained inside the container are not singletons.
spacer Fabien  — March 28, 2009 11:20   #5
@kasn: For the singleton, see my previous answer. As for the other question, please wait the following articles. For now, we do everything by hand, just to explain the concepts behind the container. I hope everything will be much more clearer and useful when I will introduce a real container class. That said, most of the time, factories use static methods to access objects; and if you have a closer look at the implementation we have talked about, there is no static method anywhere (see the Martin Fowler article for a perhaps more clearer explanation). As for mocking the transport object, it is really easy even with this hardcoded example: you can easily add a mail_transport.class parameter, and use it as the class to be used for the transport, or you can also override the getMailTransport() method.
spacer fqqdk  — March 28, 2009 14:34   #6
There is a dependency injection framework, sphicy, which is implemented in PHP, and is a port of google guice. You might want to check it out: www.beberlei.de/sphicy/
spacer Ryan Weaver  — March 28, 2009 20:02   #7
Thanks for keeping it simple, as @Ad said, i'm starting to get the big picture now.
spacer opensas  — March 29, 2009 04:43   #8
great article...

one question, wouldn't it be more confortable if the Container class had default values for the parameters array, that way you would have the benefits of having everything hardcoded in the class, with the posibility to override it if needed...
spacer Mark  — March 29, 2009 19:33   #9
@opensas: That is completely up to how you implement the container class. Like fabien said the code here was just an example to showcase how a container would work. Once a real container class is shown you will see how a default value could be set.
spacer Bogdan  — April 17, 2009 17:17   #10
Everything looks good while the only concern of the container class is mail functionality, but if you want to add further methods, you end up with very low cohesion which is bad. Instead I think it would be nice to have a container class per specific functionality and a factory for containers so you won't have $container = new Container(..), but $container = containerFactory::getInstance('mailingContainer')
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.