Powerful Dependency Injection Container

9 years ago by David Grudl  

Nette DI (Dependency Injection Container) is one of the most interesting parts of the Nette Framework. It is compiled, extremely fast and easy to configure.

Let's have an application for sending newsletters. The code is maximally simplified and is available on the GitHub.

We have the object representing email:

class Mail
{
	public $subject;
	public $message;
}

An object which can send emails:

interface Mailer
{
	function send(Mail $mail, $to);
}

Support for logging:

interface Logger
{
	function log($message);
}

And finally, a class that provides sending newsletters:

class NewsletterManager
{
	private $mailer;
	private $logger;

	function __construct(Mailer $mailer, Logger $logger)
	{
		$this->mailer = $mailer;
		$this->logger = $logger;
	}

	function distribute(array $recipients)
	{
		$mail = new Mail;
		...
		foreach ($recipients as $recipient) {
			$this->mailer->send($mail, $recipient);
		}
		$this->logger->log(...);
	}
}

The code respects Dependency Injection, ie. each object uses only variables which we had passed into it.

Also, we can implement own Logger or Mailer, like this:

class SendMailMailer implements Mailer
{
	function send(Mail $mail, $to)
	{
		mail($to, $mail->subject, $mail->message);
	}
}

class FileLogger implements Logger
{
	private $file;

	function __construct($file)
	{
		$this->file = $file;
	}

	function log($message)
	{
		file_put_contents($this->file, $message . "\n", FILE_APPEND);
	}
}

DI container is the supreme architect which can create individual objects (in the terminology DI called services) and assemble and configure them exactly according to our needs.

Container for our application might look like this:

class Container
{
	private $logger;
	private $mailer;

	function getLogger()
	{
		if (!$this->logger) {
			$this->logger = new FileLogger('log.txt');
		}
		return $this->logger;
	}

	function getMailer()
	{
		if (!$this->mailer) {
			$this->mailer = new SendMailMailer;
		}
		return $this->mailer;
	}

	function createNewsletterManager()
	{
		return new NewsletterManager($this->getMailer(), $this->getLogger());
	}
}

The implementation looks like this because:

  • the individual services are created only on demand (lazy loading)
  • doubly called createNewsletterManager will use the same logger and mailer instances

Let's instantiate Container, let it create manager and we can start spamming users with newsletters 🙂

$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);

Significant to Dependency Injection is that no class depends on the container. Thus it can be easily replaced with another one. For example with the container generated by Nette DI.

Nette DI

Nette DI is the generator of containers. We instruct it (usually) with configuration files. This is a configuration that leads to generating nearly the same class as the class Container above:

services:
	- FileLogger( log.txt )
	- SendMailMailer
	- NewsletterManager

The big advantage is the shortness of configuration.

Nette DI actually generates the PHP code of container. Therefore it is extremely fast. A developer can see the code, so he knows exactly what it is doing. He can even trace it.

The usage of Nette DI is very easy. In first, install it using Composer (cause downloading ZIP files is so outdated):

composer require nette/di

Save the (above) configuration to the file config.neon and let's create a container:

$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
	$compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;

and then use container to create object NewsletterManager and we can send e-mails:

$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);

The container will be generated only once and the code is stored in the cache (in directory __DIR__ . '/temp'). Therefore the loading of the configuration file is placed in the closure in $loader->load(), so it is called only once.

During development, it is useful to activate auto-refresh mode which automatically regenerates the container when any class or configuration file is changed. Just in the constructor ContainerLoader append TRUE as the second argument.

As you can see, using Nette DI is not limited to applications written in Nette, you can use it anywhere.