Supercharged DI Heart for Your Applications
One of the most interesting parts of Nette, praised even by users of other frameworks, is the Dependency Injection Container (hereinafter referred to as Nette DI). See how easily you can use it anywhere, even outside of Nette. [perex]
Let's consider an application for sending newsletters. I've simplified the code of the individual classes to the core. Here's an object representing an email:
class Mail
{
public $subject;
public $message;
}
Someone who knows how to send it:
interface Mailer
{
function send(Mail $mail, $to);
}
We add support for logging:
interface Logger
{
function log($message);
}
And finally, a class that manages the distribution of 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, meaning each class only works with
the variables that we have passed to it. We also have the option to
implement Mailer
and Logger
in our own way, for
example 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);
}
}
The DI container is the supreme architect that can create individual objects (referred to in DI terminology as services) and assemble and configure them precisely according to our needs.
A container for our application could look something 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 this way so that:
- the individual services are created only when needed (lazy)
- a double call to
createNewsletterManager
always uses the same logger and mailer objects
Create an instance of Container
, let it produce a manager, and
you can start spamming users with newsletters:
$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);
The essence of Dependency Injection is that no class depends on the container. Therefore, we can easily replace it with another, perhaps with a container generated by Nette DI.
Nette DI
Nette DI is indeed a container generator. We instruct it (usually) using
configuration files, and perhaps this configuration generates roughly the same
as the Container
class:
services:
- FileLogger( log.txt )
- SendMailMailer
- NewsletterManager
A significant advantage is the brevity of the notation. Additionally, we can add more and more dependencies to individual classes often without needing to modify the configuration.
Nette DI generates actual PHP container code. It is therefore extremely fast, and the programmer knows exactly what it does and can even step through it.
The container might have tens of thousands of lines in the case of large applications, and maintaining something like that manually would probably not be possible.
Deploying Nette DI into our application is very easy. First, we install it using Composer (because downloading ZIPs is so outdated):
composer require nette/di
We save the above configuration in a file config.neon
and use
the class Nette\DI\ContainerLoader
to create the container:
$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
$
compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;
and then again let it create the NewsletterManager
object and we
can start sending emails:
$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);
But back to ContainerLoader
for a moment. The mentioned syntax
is subordinate to one thing: speed. The container is generated once, its code is
written to cache (directory __DIR__ . '/temp'
), and for subsequent
requests, it is just loaded from here. Therefore, the loading of the
configuration is placed into a closure in the $loader->load()
method.
During development, it is useful to activate the auto-refresh mode, where the
container is automatically regenerated if any class or configuration file
changes. Just mention true
as the second argument in the
ContainerLoader
constructor.
As you can see, using Nette DI is definitely not limited to applications written in Nette, you can deploy it anywhere with just 3 lines of code. Try playing with it, the whole example is available on GitHub.