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ň. 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. Zkuste si s ním pohrát, celý příklad je dostupný na GitHubu.
Komentáře
Petr Sládek #1
proč to ty dvě služby udělalo jako lazyload, ale toho Newsletter managera jako továrničku?
nemělo by to v tom config.neon být spáš v sekci factories?
David Grudl #2
#1 Petr Sládek, Nette DI ke všem službám přistupuje pomocí „lazy load“, sekce factories už se nepoužívá. Kód třídy
Container
byl jen příklad, jak takový kontejner může vypadat.Richard Suchý #3
$class = $loader->load('', function($compiler) {
– ten prázdný řetězec vypadá krajně podezřele… co je to za parametr a jak jej využít?David Grudl #4
#3 Richard Suchý, první parametr je klíč, metoda
load()
je obdobná té z cache. V jednom adresáři tak může být (a často bývá) uložených víc kontejnerů, jeden třeba pro vývoj a druhý pro produkční režim, lišících se v různých parametrech, od připojení k DB až po aktivaci ladících lišt atd.Ivan #5
Pises Kód respektuje Dependency Injection, tj. že každá třída pracuje pouze s proměnnými, které jsme jí předali.
V kodu ale vidim:
Jak sis predal tridu Mail?
Aleš Roubíček #6
#5 Ivan,
Mail
není služba, ale data. Ne vše je nutné předávat, to by program nikdy nefungoval a nedělal co potřebujeme. Typicky se předávají jen věci, které z nějakého důvodu potřebujeme abstrahovat (závislosti na cizí knihovny, požadavek na polymorfní chování, nehotová implemetace modulu…)#2 David Grudl,
Container
v ukázce je ve skutečnostiServiceLocator
, není třeba dávat mu jiný název. :)David Grudl #7
#5 Ivan, zkusím to říct jinak: „třída pracuje s daty, které jsme ji předali; jiné si nezískává.“ Daty chápej třeba konfiguraci (maileru, loggeru), databázové připojení atd. Naopak deterministické funkce (což je i operátor
new
) takovou informací nejsou.Nicméně bylo by možné vytvořit továrnu na objekty
Mail
a tu předat tříděNewsletterManager
, takže místo$mail = new Mail
by volala$mail = $this->mailFactory->create()
– to je ok. Z pohledu Dependency Injection jsou obě řešení v pořádku, představují jen jiný návrh.#6 Aleš Roubíček, název Container nemám rád, protože není výstižný, nicméně Locator se sem nehodí už vůbec, neboť účelem třídy není poskytovat lookup API ke službám, ale naopak služby vytvářet a konfigurovat. Třeba ServiceFactory by bylo výstižnější.
Aleš Roubíček #8
#7 David Grudl, když se podívám na Bliki o ServiceLocatoru, tak moc velký rozdíl oproti tvému Containeru nevidím. :) Žádný lookup, ale sada factory metod poskytující jednotlivé služby.
Filip Procházka #9
#8 Aleš Roubíček, Je velký rozdíl v tom, když máš na vstupu do aplikace jedno vytažení z containeru jedné služby, kterou pak aplikaci spustíš a když tím máš posetou celou aplikaci a v modelech vyžaduješ container, nikoliv konkrétní služby. Pokud v aplikaci jako takové používáš container přímo, tak ho degraduješ na service locator. Ale není přece možné inicializovat to vzduchem magicky, minimálně nějaký front controller na kterém zavoláš run, vždy vytáhnout musíš.
David Grudl #10
#8 Aleš Roubíček, podívej se lépe.
ServiceLocator
(a vystihuje to i název) funguje jako singleton registry, neumí objekty vyrábět, jen je vracet. Tedy někdo jej musí instancemi naplnit. Naopak úkolem Containeru je služby vyrábět (metodygetLogger()
agetMailer()
mohly klidně být private).nepovím #11
Díky, pro začátečníky jako já super čtení.
lenoch #12
A na ServiceLocatoru je špatně to, že nepředávám výslovně potřebné objekty skrz konstruktor jako u DI?
Mě totiž přijde jako klíčová featura to, že si můžu někde nakonfigurovat jaké objekty se budou čemu předávat, což umožňuje jak DIContainer, tak ServiceLocator (imho).
To že se objekt „hlásí“ ke svým závislostem, aspoň mě osobně zas tak významné nepřijde. Stejně musím (v případě DI) bádat v nějakém kontejneru nebo konfiguračním souboru, co tam vlastně odkud probublává, ne?
David Grudl #13
#12 lenoch, mám tu o tom celou řadu článků. Stručně:
Taco #14
#12 lenoch, Nevědomky sis odpověděl sám. Díky tomu, že se objekt hlásí ke svým závislostem, tak bádat nemusíš (výjimka mě napadá, kdy máš více různých implementací téhož, ale to bude IMHO opravdu zřídka).
Igor Hlina #15
Davide Grudle mohol by si sa trocha rozpisat o tom preco je
$loader->load()
pouziva anonymnu funkciu?<cite>
Uvedený zápis je podřízen jediné věci: rychlosti. Kontejner se vygeneruje jednou, jeho kód se zapíše do cache 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ě.
</cite>
Ako presne toto funguje?
David Grudl #16
#15 Igor Hlina, Nemusí to být anonymní funkce, může to být i metoda. Jde jen o tom, že kód ve funkci se vykoná pouze jednou, vygeneruje se kód kontejneru, uloží do cache a při dalších požadavcích se už používá cache.
Kubo2 #17
#16 David Grudl, Takže vlastne ide iba o to, aby sa nevytváral objekt $compiler, pokiaľ to nie je potrebné?
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.