Nabušené DI srdce pro vaše aplikace

před 6 lety od David Grudl     edit

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.