Nabušené DI srdce pro vaše aplikace
Jednou z nejzajímavějších částí Nette, kterou vychvalují i uživatelé jiných frameworků, je Dependency Injection Container (dále Nette DI). Podívejte se, jak snadno jej můžete použít kdekoliv, i mimo Nette.
Mějme aplikaci pro rozesílání newsletterů. Kód jednotlivých tříd jsem zjednodušil na dřeň. Celý příklad je dostupný na GitHubu.
Máme tu objekt představující email:
class Mail
{
public $subject;
public $message;
}
Někoho, kdo ho umí odeslat:
interface Mailer
{
function send(Mail $mail, $to);
}
Přidáme podporu pro logování:
interface Logger
{
function log($message);
}
A nakonec třídu, která rozesílání newsletterů zajišťuje:
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(...);
}
}
Kód respektuje Dependency Injection, tj. že každá třída pracuje
pouze s proměnnými, které jsme jí předali. Také máme možnost si
Mailer
i Logger
implementovat po svém,
třeba takto:
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 kontejner je nejvyšší architekt, který umí stvořit jednotlivé objekty (v terminologii DI označované jako služby) a poskládat a nakonfigurovat je přesně podle naší potřeby.
Kontejner pro naši aplikaci by mohl vypadat třeba takto:
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());
}
}
Implementace vypadá takto, aby:
- se jednotlivé služby vytvářely, až když je potřeba (lazy)
- dvojí volání
createNewsletterManager
využívalo stále stejný objekt loggeru a maileru
Vytvoříme instanci Container
, necháme ji vyrobit managera a
můžeme se pustit do spamování uživatelů newslettery:
$container = new Container;
$manager = $container->createNewsletterManager();
$manager->distribute(...);
Podstatné na Dependency Injection je, že žádná třída nemá závislost na kontejneru. Tudíž jej můžeme klidně nahradit za jiný. Třeba za kontejner, který nám vygeneruje Nette DI.
Nette DI
Nette DI je totiž generátor kontejnerů. Instruujeme ho (zpravidla) pomocí
konfiguračních souborů a třeba tato konfigurace vygeneruje cca totéž, jako
byla třída Container
:
services:
- FileLogger( log.txt )
- SendMailMailer
- NewsletterManager
Zásadní výhodou je stručnost zápisu. Navíc jednotlivým třídám můžeme přidávat další a další závislosti často bez nutnosti do konfigurace zasahovat.
Nette DI vygeneruje skutečně PHP kód kontejneru. Ten je proto extrémně rychlý, programátor přesně ví, co dělá, a může ho třeba i krokovat.
Kontejner může mít v případě velkých aplikací desetitisíce řádků a udržovat něco takového ručně by už nejspíš ani nebylo možné.
Nasazení Nette DI do naší aplikace je velmi snadné. Nejprve jej nainstalujeme Composerem (protože stahování zipů je tááák zastaralé):
composer require nette/di
Výše uvedenou konfiguraci uložíme do souboru config.neon
a
pomocí třídy Nette\DI\ContainerLoader
vytvoříme kontejner:
$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp');
$class = $loader->load(function($compiler) {
$compiler->loadConfig(__DIR__ . '/config.neon');
});
$container = new $class;
a pak jej opět necháme vytvořit objekt NewsletterManager
a
můžeme rozesílat emaily:
$manager = $container->getByType('NewsletterManager');
$manager->distribute(['john@example.com', ...]);
Ale ještě na chvíli zpět ke ContainerLoader
. Uvedený zápis
je podřízen jediné věci: rychlosti. Kontejner se vygeneruje jednou, jeho
kód se zapíše do cache (adresář __DIR__ . '/temp'
) a při
dalších požadavcích se už jen odsud načítá. Proto je načítání
konfigurace umístěno do closure v metodě
$loader->load()
.
Během vývoje je užitečné aktivovat auto-refresh mód, kdy se kontejner
automaticky přegeneruje, pokud dojde ke změně jakékoliv třídy nebo
konfiguračního souboru. Stačí v konstruktoru ContainerLoader
uvést jako druhý argument true
.
Jak vidíte, použití Nette DI rozhodně není limitované na aplikace psané v Nette, můžete jej pomocí pouhých 3 řádků kódu nasadit kdekoliv.
Chcete-li odeslat komentář, přihlaste se