2. Do you need a dependency injection container?

Posted by Unholy Prayer on Mon, 10 Jun 2019 20:28:27 +0200

This is an article I translated from a website abroad. Do you need a Dependency Injection Container?

This article is part of a series of articles on dependency injection and PHP lightweight container implementations:
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 article in this series on dependency injection, I tried to give a web example of dependency injection, and today I'm going to talk about dependency injection containers.

First, let's look at a bold statement: "In most cases, you can benefit from dependency injection without requiring a dependency injection container."

But when you have many objects that need to solve many dependencies, a dependency injection container can be very useful (think about those frameworks). If you remember the example in the first article, creating a User object requires creating a SessionStorage object first. This is not a big problem, but you need to know at least what dependencies you need before you create the required objects:

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

In the next article, I'll talk about the implementation of dependency injection containers in the Symfony 2 framework. But one thing I want to make clear is that this implementation has nothing to do with the Symfony framework itself. I will use the example in the Zend framework to illustrate it.

The Mail Library in the Zend framework simplifies mail operations and uses the PHP mail() function to send mail, but this is not very flexible. Thankfully, this can be easily changed by providing a transport object. The following code shows how to create a Zend_Mail object to send mail 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);

A dependency injection container is a container that knows how to initialize and configure objects. To accomplish this task, it needs to know the relationship between constructor parameters and objects.
Here is a simple hard-coded container for the previous 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 containers is simple:

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

When using containers, we only need a mailer object. We don't need to know how to create it. Everything about mailer object creation is encapsulated inside the container.
However, a smart reader may find that the container is hard-coded. So we need to further optimize and add some parameters so that the container will be more 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;
  }
}

Now you can easily pass parameters such as username and password through the constructor:

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

If you need to change the mailer class to test, the name of the object can also be passed by 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();

Finally, but not least, every time I want a mailer, I don't have to create a new instance, so the container can be transformed to return the same object every time:

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;
  }
}

By introducing a static $shared property, each time you call getMailer(), you return the object you first created.

Containers wrap the basic features that need to be implemented and rely on injection of container-managed objects: from initialization to configuration. Objects themselves do not know that they are managed by containers, let alone containers. So the container can manage any PHP object, and it would be better if the object used dependency injection to resolve dependencies, but that was not a prerequisite.

Of course, creating and maintaining container classes manually can be a nightmare. But the requirement for a usable container is still very low and easy to implement. In the next chapter, I will introduce the implementation of dependency injection in Symfony 2 framework.

Topics: SSL PHP xml