[2011–05–05] Finalizace Dependency Injection
- David Grudl
- Nette Core | 8218
Co je to vůbec DI kontejner? Podstatou Dependency Injection (DI) je odebrat třídám zodpovědnost za získávání konkrétních objektů (tzv. služeb), které potřebují ke své činnosti. Místo toho služby dostávají už při svém vytváření a jsou jim předávány (vstřikovány) buď konstruktorem nebo přes settery. Právě o instancování a složení dohromady se stará DI kontejner. Více
1) Přejmenování na Nette\DI\Container
S odstupem několika měsíců jsem si uvědomil, jak špatný je název třídy Context. Jistě, kontejner může být a obvykle bude použit pro uložení „kontextu“ tvořeného servisy, ale tohle třída nemůže předjímat a z obecného pohledu to stále je kontejner. A vzhledem k vývoji jmenných prostorů a převodníku pro 5.2 verzi se už obecného pojmenování „kontejner“ nebojím.
2) Úprava kontejneru
Container má oproti dřívějšímu Context nebo ServiceLocator čistší a pohodlnější API. Z metody addService zmizely parametry $options a $singleton. Callbackům se nyní místo parametru $options předává samotný kontejner.
V kontejneru naopak přibylo pole $params pro uživatelské parametry a také „magické settery/gettery“ jakožto zkratky pro addService, getService, hasService a removeService. Doplnil jsem ještě podporu pro statické továrničky a experimentální ServiceBuilder, viz níže.
To je vlastně vše.
Nebuďte zklamaní :-) Zjišťuju, že pokud jde o DI, tak je mezi programátory rozšířeno mnoho polopravd a mýtů. Bohužel, mnozí se jimi řídí ;) Přidám proto raději několik use-cases.
3) use cases
Příklady se pokusím vytvořit podobně jako u symfony, protože z příspěvků na fóru vidím, že často je právě symfony v souvislosti s DI zmiňováno.
Nejprve poznámka: Container/Context/ServiceLocator nijak nesouvisí se třídou Environment! Na Environment klidně zapomeňte, pokud ho nechcete.
Dynamický kontejner s definicí služeb pomocí callbacků:
$cont = new Nette\DI\Container;
$cont->addService('mailer', function($cont) {
return new Nette\Mail\SmtpMailer(array(
'host' => 'smtp.gmail.com',
'username' => $cont->params['username'], // údaje bereme z parametrů
'password' => $cont->params['password'],
'secure' => 'ssl',
));
});
$cont->addService('message', function($cont) {
$message = new $cont->params['message.class'];
$message->setMailer($cont->mailer); // zkratka pro $this->getService('mailer')
$message->setFrom('John Doe <doe@example.com>');
return $message;
});
// nastavíme parametry
$cont->params = array(
'username' => 'john',
'password' => '***',
'message.class' => 'Nette\Mail\Message',
);
// a nyní kontejner použijeme pro vytvoření objektu emailu:
$message = $cont->message; // zkratka pro $cont->getService('message')
Jednotlivé services jsou sdílené, tj. vytváří se jen jednou.
Pokud místo callbacku předáme řetězec s názvem třídy, vytvoří se objekt ServiceBuilder, jehož úkolem je později třídu instancovat. Metoda addService onen builder vrací. Pokud bychom tedy vytvořili builder s metodami jako addArgument nebo addMethodCall, bylo by možné uvedený příklad zapsat třeba takto:
$cont->addService('mailer', new AnotherBuilder('Nette\Mail\SmtpMailer'))
->addArgument(array(
'host' => 'smtp.gmail.com',
'username' => '%username%',
'password' => '%password%',
'secure' => 'ssl',
));
$cont->addService('message', new AnotherBuilder('%message.class%'))
->addMethodCall('setMailer', '@mailer')
->addMethodCall('setFrom', 'John Doe <doe@example.com>');
$cont->params = array(
'username' => 'john',
'password' => '***',
'message.class' => 'Nette\Mail\Message',
);
Zdůrazňuji, že AnotherBuilder v Nette přímo není, protože mi to připadá jako škrabání se přes hlavu. Nevidím moc smysl ve vytváření nového metajazyka, když to jde jednoduše pomocí callbacků. Ale proti gustu… Kód AnotherBuilder by vypadal cca takto:
class AnotherBuilder extends Nette\DI\ServiceBuilder
{
private $args = array();
private $calls = array();
public function addArgument($arg)
{
$this->args[] = $arg;
return $this;
}
public function addMethodCall($method)
{
$this->calls[] = func_get_args();
return $this;
}
public function createService(Nette\DI\IContainer $container)
{
$class = $container->expand($this->class);
try {
$type = new Nette\Reflection\ClassType($class);
} catch (\ReflectionException $e) {
throw new Nette\InvalidStateException("Cannot instantiate service, class '$class' not found.");
}
$expander = function(& $val) use ($container) {
$val = $val[0] === '@' ? $container->getService(substr($val, 1)) : $container->expand($val);
};
$args = $this->args;
array_walk_recursive($args, $expander);
$service = $type->newInstanceArgs($args);
foreach ($this->calls as $call) {
array_walk_recursive($call, $expander);
call_user_func_array(array($service, array_shift($call)), $call);
}
return $service;
}
}
Statický kontainer
Nová je podpora pro statické továrničky. Tj. místo dynamického
vkládání callbacků můžeme vytvořit třídu (potomka Container), který
bude sadu metod-továrniček rovnou obsahovat. Jejich název musí mít tvar
createServiceXyz
:
class MailContainer extends Nette\DI\Container
{
protected function createServiceMailer()
{
return new Nette\Mail\SmtpMailer(array(
'host' => 'smtp.gmail.com',
'username' => $this->params['username'],
'password' => $this->params['password'],
'secure' => 'ssl',
));
}
protected function createServiceMessage()
{
$message = new $this->params['message.class'];
$message->setMailer($this->mailer);
$message->setFrom('John Doe <doe@example.com>');
return $message;
}
}
$cont = new MailContainer;
$cont->params = array(
'username' => 'john',
'password' => '***',
'message.class' => 'Nette\Mail\Message',
);
$message = $cont->message;
Všechny služby v rámci kontejneru jsou trvalé, tj. při volání
getService() (či získání přes proměnou) se nevytváří služba
opakovaně, ale vrací se již dříve vytvořená. Jakékoliv jiné chování
by bylo leda matoucí. V uvedeném příkladu by se ale hodilo, kdyby kontejner
pokaždé vygeneroval nový objekt Nette\Mail\Message
. Toho lze
dosáhnout snadno: namísto služby message
vytvoříme obyčejnou
metodu createMessage
:
class MailContainer extends Nette\DI\Container
{
function createServiceMailer()
{
return new Nette\Mail\SmtpMailer(array(
'host' => 'smtp.gmail.com',
'username' => $this->params['username'],
'password' => $this->params['password'],
'secure' => 'ssl',
));
}
function createMessage()
{
$message = new $this->params['message.class'];
$message->setMailer($this->mailer);
$message->setFrom('John Doe <doe@example.com>');
return $message;
}
}
$cont = new MailContainer;
$cont->params = array(
'username' => 'john',
'password' => '***',
'message.class' => 'Nette\Mail\Message',
);
$mailer = $cont->createMessage();
Z kódu $xyz = $cont->createXyz()
je zřejmé, že se
vytváří pokaždé nový objekt. Jde tedy o konvenci na straně
programátora.
Jontejner je možné zmrazit a poté ho již nelze měnit:
$container->freeze();
$container->addService(...); // vyhodí výjimku
Rozmrazíte jej vytvořením klonu, viz níže.
Aliasování
Vytváření aliasů pro názvy služeb se mi jeví spíš jako chyba návrhu a doporučil bych se mu vyhnout. Pokud by však nebylo zbytí, můžete službu jednoduše vložit pod jiným názvem:
$container->addService('alias', $container->getService('originalName'));
Pochopitelně tímto způsobem můžete kopírovat služby mezi jednotlivými kontejnery.
Pokud chcete zachovat lazy-loading, tj. kopírovat služby, které zatím nejsou vytvořené, udělejte to následovně:
$container->addService('alias', function($container) {
return $container->getService('originalName');
});
Nebo mezi dvěma kontejnery:
$containerDest->addService('mailer', function() use ($containerSrc) {
return $containerSrc->getService('mailer');
});
Kontejner je také možné klonovat. Klon obsahuje všechny služby jako vzor a pochopitelně lze do něj přidávat nové.
$dolly = clone $container;
Auto-wiring
Autowire umožňuje při vytváření služby automaticky předávat do konstruktoru další služby dle type hintů. Vytvoříme si k tomu nový ServiceBuilder:
class AutowireServiceBuilder extends Nette\DI\ServiceBuilder
{
public function createService(Nette\DI\IContainer $container)
{
try {
$type = new Nette\Reflection\ClassType($this->class);
} catch (\ReflectionException $e) {
throw new Nette\InvalidStateException("Cannot instantiate service, class '$this->class' not found.");
}
$args = array();
if ($type->hasMethod('__construct')) {
foreach ($type->getMethod('__construct')->getParameters() as $param) {
if ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} elseif ($param->getClass()) {
$args[] = $container->getServiceByType($param->getClass()->getName());
} else {
$args[] = $param->isArray() && !$param->allowsNull() ? array() : NULL;
}
}
}
return $type->newInstanceArgs($args);
}
}
// přidáme službu, která se sestaví auto-wiringem
$cont->addService('message', new AutowireServiceBuilder('Message'));
Klíčem je metoda kontejneru getServiceByType()
, která vrátí
službu podle daného typu (třída, interface). Taková služba musí být
v kontejneru právě jedna, jinak se vyhodí výjimka.
Pokud přidáváte službu pomocí továrny, není její typ pochopitelně znám. Můžete ho proto „napovědět“ třetím parametrem metody addService:
$cont->addService('mailer', function($cont) { ... }, 'Nette\Mail\IMailer');
K ověření typu služby slouží metoda checkServiceType():
if (!$cont->checkServiceType('mailer', 'Nette\Mail\IMailer')) {
...
}
Tagování
Při uložení jakéhokoliv objektu můžeme u něj uvést doplňující metainformace, tzv. tagy:
$container->addService('mailer', ..., array('tag1', 'tag2'));
A poté můžeme v kontejneru vyhledat všechny služby (resp. jejich jména), které mají daný tag:
$list = $container->getServiceNamesByTag('tag1')
Tag nemusí být je řetězec, ale může obsahovat další libovolné atributy:
$container->addService('mailer', ..., array(
'tag1' => array('id' => 'mx', 'priority' => 12),
'tag2' => array('...'),
));
$list = $container->getServiceNamesByTag('tag1')
// vrací pole array('mailer' => array('id' => 'mx', 'priority' => 12))
- Honza Marek
- Člen | 1664
Budou v containeru defaultně jako parametry config a environment variables?
Jde vyměnit výchozí ServiceBuilder pro všechny servisy?
Editoval Honza Marek (5. 5. 2011 7:27)
- David Grudl
- Nette Core | 8218
Honza Marek napsal(a):
Budou v containeru defaultně jako parametry config a environment variables?
Pochopitelně NE – kde by se to tam vzalo? Co je to Environment a jak to svouvisí s DI?
Jde vyměnit výchozí ServiceBuilder pro všechny servisy?
Myslíš pro své kontejnery? Vlastní ServiceBuilder může být parametrem addService.
- Filip Procházka
- Moderator | 4668
Chtělo by to ještě ty Compilery
a nastavitelný
IServiceBuilder
:) Jinak to vypadá opravdu dobře! A taky mi
konečně docvaklo na co je dobré rozšiřování containeru aka
MailContainer
:D Taky šikovné, byť trošku magické.
//Edit: Ne Honza myslel nastavit IServiceBuilder
globálně
(vyměnit)
$container = new Container();
$container->setServiceBuilderClass('MyServiceBuilderClass');
$builder = $container->addService('one', 'StupSomething');
$builder instanceof MyServiceBuilderClass;
Editoval HosipLan (5. 5. 2011 8:27)
- Ondřej Mirtes
- Člen | 1536
Super, konečně to začíná vypadat k světu :) Mám taky několik poznámek:
- ServiceBuilder bych určitě do frameworku dal, bude důležitý pro konfiguraci kontejneru v configu.
- Kontejner by mělo interně využívat samotné Nette, a to tak, že si
pomocí něj nechá sestavovat většinu objektů. V důsledku by se pak
operátor
new
měl objevit jen na pár speciálních místech. Umožní to snadnou vyměnitelnost všeho, co framework využívá. To se dnes děje pomocí pole$defaultServices
, já bych tu myšlenku rozšířil a aplikoval na vše. V praxi si to představuji tak, že přímo v distribuci budedefault.neon
s definicemi servis, do něhož se pak bude mergovat aplikačníconfig.neon
, čímž si budu moct na jednom místě a jednotně cokoli přepsat. - Líbí se mi sestavování presenterů pomocí DI kontejneru. Presenter si v konstruktoru nadefinuje, jaké servisy chce a PresenterFactory mu je při vytvoření poskytne. Je pak na první pohled jasné, co daný presenter všechno potřebuje a zajišťujeme tak, že si nesáhne na něco, o co si neřekl. V praxi by pak bylo potřeba do ServiceBuilderu doplnit podporu pro injektování servis do všech potomků nějakého předka, pokud si např. můj BasePresenter sám nějakou závislost nadefinuje. A pokud by to někdo nechtěl takto dělat, jednoduše si nadefinuje, že BasePresenter pouze přijímá DI kontejner a má po starostech. Tohle bude chtít autowiring, abych se neupsal.
- Parametry – hodilo by se tam mít appDir, wwwDir, libsDir, basePath, opět kvůli tomu, abych je mohl injektovat do servis a nemusel používat ty ošklivé konstanty. A hlasuji proto, aby tam byly už z výroby (tedy z nějaké továrny, která samotný kontejner sestaví), protože to povede programátory k tomu, aby nepoužívaly ty konstanty.
- Ondřej Brejla
- Člen | 746
Jen jednu kosmetickou, co mě uhodila do očí. Nebylo by lepší, aby byla konvence pojmenovávání service továrniček stejná, jako u továrniček na komponenty? Rozumějte: createWhateverService() vs. createComponentWhatever()
- Honza Marek
- Člen | 1664
David Grudl napsal(a):
Pochopitelně NE – kde by se to tam vzalo? Co je to Environment a jak to svouvisí s DI?
Samozřejmě by se tyhle parametry nevyráběly přímo v třídě
Container
, ale mohly by se plnit v nějaké továrničce na
výchozí globální container. S DI to souvisí tak, že je lepší vytvářet
službu už nastavenou podle konfigurace, než že ve službě si budu tahat
nějaké $context->config->blabla
. Například taková
továrnička na vyrobení nějakého databázovátka by jistě měla mít po
ruce parametry připojení, přijde mi logické to mít už v parametrech
Containeru.
HosipLan
Kompilery nad closurama udělat nepůjdou. Jedině nad nějakýma vlastníma IServiceBuilderama.
- Honza Marek
- Člen | 1664
- Tharos
- Člen | 1030
Mám dotaz, jestli je v současných zdrojácích Nette používání obou
termínů context
i container
záměrem (podle místa
použití), anebo zda se to ještě bude sjednocovat. Chtěl bych se vyhnout
tomu, aby se mi při refaktoringu za účelem udržení jednoty s Nette
konvencemi ony konvence změnily pod rukami. :)
- Patrik Votoček
- Člen | 2221
David Grudl napsal(a):
Zdůrazňuji, že takovýto ServiceBuilder v Nette přímo není, protože mi to připadá jako škrabání se přes hlavu. Nevidím moc smysl ve vytváření nového metajazyka, když to jde jednoduše pomocí callbacků. Ale proti gustu…
Existence něčeho takového má smysl pouze v případě definování složitějších služeb přímo v configu.
David Grudl napsal(a):
Honza Marek napsal(a):
Budou v containeru defaultně jako parametry config a environment variables?
Pochopitelně NE – kde by se to tam vzalo? Co je to Environment a jak to svouvisí s DI?
Tohle je hodně klíčová vlastnost! Bez které mě DI moc nedává smysl. Jak už psal honza třeba kvuli údajům o připojení k DB. Nebo k nadefinování SMTP připojení.
Celé tyto dva postoje mě tak trochu připadají jako by mělo být v Nette možné pracovat s DI pouze v PHP. A možnost definice či jiné práce s kontejnerem v configu úplně odstřihnout. Což se mě nelíbí ani trochu.
Co se týká environment variables a configu a teď vlastně ještě container params. Tak bych byl pro absolutního sjednocení pouze do container params.
Ondřej Brejla napsal(a):
Jen jednu kosmetickou, co mě uhodila do očí. Nebylo by lepší, aby byla konvence pojmenovávání service továrniček stejná, jako u továrniček na komponenty? Rozumějte: createWhateverService() vs. createComponentWhatever()
Taky mě to zarazilo a napadlo (už včera ale to ještě neexistovalo tohle vlákno).
Tharos napsal(a):
Mám dotaz, jestli je v současných zdrojácích Nette používání obou termínů
context
icontainer
záměrem (podle místa použití), anebo zda se to ještě bude sjednocovat. Chtěl bych se vyhnout tomu, aby se mi při refaktoringu za účelem udržení jednoty s Nette konvencemi ony konvence změnily pod rukami. :)
Je to fakt hodně čerstvé. Záměrem to jistě není. Mělo by to být všude jako container.
- Patrik Votoček
- Člen | 2221
Ještě pokud nebude ani jedna z námitek / nápadů implementována
udělám si novou nadstavbu nad Nette DI a aby se mě to dělalo lépe
potřeboval bych: Nette\DI\IConfigurator
(pull)
Editoval Patrik Votoček (5. 5. 2011 16:05)
- Honza Marek
- Člen | 1664
Patrik Votoček napsal(a):
Co se týká environment variables a configu a teď vlastně ještě container params. Tak bych byl pro absolutního sjednocení pouze do container params.
Přesně tak.
- bene
- Člen | 82
Honza Marek napsal(a):
Budou v containeru defaultně jako parametry config a environment variables?
Jde vyměnit výchozí ServiceBuilder pro všechny servisy?
Resit se to da asi takto:
$cont = new Nette\DI\Container;
$cont->addService('config', function() {
return Nette\Environment::getConfig();
});
$cont->addService('db', function($cont) {
$conn = new DibiConnection($cont->config->database);
return $conn;
});
Jinak prijde mi to prijemne jednoduche…
- Patrik Votoček
- Člen | 2221
bene napsal(a):
Resit se to da asi takto:
…
Jinak prijde mi to prijemne jednoduche…
To víme že to tak jde řešit ale naším cílem je 100% se vyhnout
volání Environment
. Tohle je jenom taková klička a vůbec to
není pěkné!
Nehledě na to že existují v podstatě tři různá úložiště „konfiguračních“ proměnných.
arron napsal(a):
Ja jsem ten kod zatim nezkoumal moc podrobne, ale ono uz nejde mit sluzby v configu??
Jde…
Editoval Patrik Votoček (5. 5. 2011 20:39)
- David Grudl
- Nette Core | 8218
Jsem skutečně rád, že se vám zbrusu nový container líbí!
A teď budu trošku ošklivý :-) Víte, jak by se stejný příklad s dynamickým kontejnerem udělal ve stařičkém pradávném špatném a překonaném Service Locatoru? Takto:
$cont = new Nette\ServiceLocator;
$cont->addService('mailer', function() use ($cont) {
return new Nette\Mail\SmtpMailer(array(
'host' => 'smtp.gmail.com',
'username' => $cont->params['username'],
'password' => $cont->params['password'],
'secure' => 'ssl',
));
});
$cont->addService('message', function() use ($cont) {
$message = new $cont->params['message.class'];
$message->setMailer($cont->getService('mailer'));
$message->setFrom('John Doe <doe@example.com>');
return $message;
});
$cont->params = array(
'username' => 'john',
'password' => '***',
'message.class' => 'Nette\Mail\Mail',
);
$message = $cont->getService('message');
Tak trošku to samé, co? Jde o verzi z 23.9.2010, jen jsem přidal public $params. Nic víc. Takže pokud máme být korektní a féroví, ehm, zakopaný pes skutečně není v podpoře DI na straně frameworku, ale podpoře DI v naší/vaší mysli. Chce to nadhled, přátelé.
- Patrik Votoček
- Člen | 2221
Nechci aby to vyznělo blbě. Ale o nepodpoře DI na straně frameworku se tu nikdo nepře. Jen jsou tu argumenty že stará implementace a ta nová je sice lepší ale né o moc.
Tvůj komentář se dá chápat dvěma způsoby a to tak že je nová implementace obrovský krok v před i když stará byla takřka stejná. A je cool a nic se na ní měnit nebude. Nebo tak že je tohle celé jenom začátek něčeho co teprve příjde (dnes/zítra/…). Sám nevím kterou z těch možností zvolit ale v koutku duše doufám ve druhou variantu. (a nebo sem to vůbec nepochopil a jdu se zahrabat)
Editoval Patrik Votoček (6. 5. 2011 1:43)
- David Grudl
- Nette Core | 8218
//Edit: Ne Honza myslel nastavit
IServiceBuilder
globálně (vyměnit)
Globálně. Ech. Globálně nastavit IServiceBuilder je popliváním všechno, co nějak souvisí s DI.
Samozřejmě by se tyhle parametry nevyráběly přímo v třídě
Container
, ale mohly by se plnit v nějaké továrničce na výchozí globální container.
Environment === globální container. Nic víc, nic míň. Samozřejmě tu k nějakému refaktoringu dojde (a celkem zásadnímu), ale obecně, mluvit jedním dechem o zrušení Environment a zároveň potřebě globálního containeru (tj. Environment) je pozoruhodné.
Ostatně soudím, že Environment musí být zničen. (Cato)
Patrik Votoček napsal(a):
Existence něčeho takového má smysl pouze v případě definování složitějších služeb přímo v configu.
Jasně – otázka zní: proč definovat složitější služby v configu? Nenapadá mě jediná výhoda. Nejde to pořádně krokovat, debugovat, vyžaduje to naučení dalšího jazyka, je to pomalejší, je to méně ohebné, je to prostě jen momentální móda.
Co se týká environment variables a configu a teď vlastně ještě container params. Tak bych byl pro absolutního sjednocení pouze do container params.
Stačí si uvědomit, že Environment = globální container a pak se jeví variables vs. config vs. container params jen jako implementační detail. (Ale sjednocení do params je v plánu)
- David Grudl
- Nette Core | 8218
Tharos napsal(a):
Mám dotaz, jestli je v současných zdrojácích Nette používání obou termínů
context
icontainer
záměrem (podle místa použití), anebo zda se to ještě bude sjednocovat.
Současné použití je záměrné, zdá se mi to tak rozumné. Název proměnné vystihuje smysl objektu Container.
- David Grudl
- Nette Core | 8218
Patrik Votoček napsal(a):
Nikoliv, snažím se poukázat na to, jak plné jsou tyto diskuse dojmologie a jak nejasný, rozmlžený a nejednotný je cíl. Je to v podstatě totéž, jako diskuse o jmenných prostorech, jen se to zdánlivě jeví v souhlasném hávu.
- Patrik Votoček
- Člen | 2221
David Grudl napsal(a):
Globálně. Ech. Globálně nastavit IServiceBuilder je popliváním všechno, co nějak souvisí s DI.
Tak takhle to Honza určitě nemyslel. Podle mě myslel spíš něco takového:
class Container extends \Nette\FreezableObject implements IContainer
{
private $serviceBuilderClass;
public function setDefaultServiceBuilderClass($class)
{
$this->serviceBuilderClass = $class;
}
}
Nevím jak to slovy popsat rychleji… Proto raději kód…
Jasně – otázka zní: proč definovat složitější služby v configu? Nenapadá mě jediná výhoda. Nejde to pořádně krokovat, debugovat, vyžaduje to naučení dalšího jazyka, je to pomalejší, je to méně ohebné, je to prostě jen momentální móda.
Tou výhodou je rychlé přidáni „unikátního“ prostředí (environmentu). Ale oproti ostatním nevýhodám je to slabý argument. Beru! (Hlavně ten dovětek :-) )
Stačí si uvědomit, že Environment = globální container a pak se jeví variables vs. config vs. container params jen jako implementační detail. (Ale sjednocení do params je v plánu)
A právě protože je globální tak zrušit! :-)
Btw mohl by mě někdo vysvětlit výhody klonování kontaineru (asi mě pořád unikají)?
Nikoliv, snažím se poukázat na to, jak plné jsou tyto diskuse dojmologie a jak nejasný, rozmlžený a nejednotný je cíl. Je to v podstatě totéž, jako diskuse o jmenných prostorech, jen se to zdánlivě jeví v souhlasném hávu.
Díky za objasnění…
Editoval Patrik Votoček (6. 5. 2011 2:21)
- David Grudl
- Nette Core | 8218
Tedy jde o rozdíl mezi:
$cont->serviceBuilderClass('MyServiceBuilder');
$cont->addService('message', '%message.class%')
->addMethodCall('setMailer', '%mailer%')
->addMethodCall('setFrom', 'John Doe <doe@example.com>');
a
$cont->addService('message', new MyServiceBuilder('%message.class%'))
->addMethodCall('setMailer', '%mailer%')
->addMethodCall('setFrom', 'John Doe <doe@example.com>');
To druhé funguje teď a připadá mi to zásadně lepší z hlediska čistoty kódu.
- Filip Procházka
- Moderator | 4668
David Grudl napsal(a):
Globálně. Ech. Globálně nastavit IServiceBuilder je popliváním všechno, co nějak souvisí s DI.
Samozřejmě tak doslova jsem to nemyslel, jenom pro určitou instanci Containeru… Měl jsem napsaný útržek kódu podobný tomu Patrikovému, ale pak jsem to smazal :)
Samozřejmě výhoda se projeví v momentě, kdy chceš definovat těch služeb více a taky když je máš v configu. Tam to současná implementace vyloženě odmítá.
- Honza Marek
- Člen | 1664
On to nebude nakonec takový problém, když si člověk vyrobí na Container nějaké plnítko, které to new MyServiceBuilder udělá za něj.
Jinak definice jinak než pomocí callbacků dává smysl, pokud je potom možné ty definice projít a upravit. Přidat volání setterů, udělat autowiring a tak. To v symfony jde pomocí kompilátorů containeru. S rychlostí taky nemají problém, protože ContainerBuilder umožňující tuhle pokročilou definici se nakonec vydumpuje do třídy extendující Container, kde se přímo v kódu nachází továrničky na služby podobně jako v Nettím containeru.
- Yrwein
- Člen | 45
Jasně – otázka zní: proč definovat složitější služby v configu? Nenapadá mě jediná výhoda. Nejde to pořádně krokovat, debugovat, vyžaduje to naučení dalšího jazyka, je to pomalejší, je to méně ohebné, je to prostě jen momentální móda.
Protože config slouží ke konfiguraci a tou „módou“ je možnost konfigurovat služby pro různá prostředí (pro development třeba budu potřebovat jiný mailer než pro production). :) (A pokud by byla (edit) řeč třeba o Symfony2 containeru, tak ten jako cache používá vygenerovanou třídu, takže je pak k dispozici to, co bychom jinak psali v PHP — čímž zároveň odpadá ona pomalost.)
Jinak nechápu, jak může být konfigurace méně ohebná než psaní service v PHP..? (Co je myšleno ohebností?)
Editoval Yrwein (6. 5. 2011 10:01)
- David Grudl
- Nette Core | 8218
Honza Marek napsal(a):
On to nebude nakonec takový problém, když si člověk vyrobí na Container nějaké plnítko, které to new MyServiceBuilder udělá za něj.
Přesně tak, co „chybí“ je ContainerBuilder, samotný Container netřeba ohýbat. Takový minibuilder je v podstatě součástí loadConfig(), možná by nebylo špatné ho vyčlenit to samostatné třídy.
- Patrik Votoček
- Člen | 2221
David Grudl napsal(a):
Přesně tak, co „chybí“ je ContainerBuilder, samotný Container netřeba ohýbat. Takový minibuilder je v podstatě součástí loadConfig(), možná by nebylo špatné ho vyčlenit to samostatné třídy.
To by nebylo vůbec špatné!!! Vote++
- Patrik Votoček
- Člen | 2221
Super
Nápad na vylepšení opět rychleji pochopitelné z kódu (kromě již zmiňovaného sjednocení params):
interface IContainerBuilder
{
public function addConfigurator(IConfigurator $configurator);
/**
* @return IContainer
*/
public function createContainer();
}
interface IConfigurator
{
public fucntion process(IContainer $container);
}
class ContainerBuilder implements IContainerBuilder
{
private $configurators = array();
public function addConfigurator(IConfigurator $configurator)
{
$this->configurators[] = $configurator;
}
public function createContainer()
{
$container = new Container;
foreach ($this->configurators as $configurator) {
$configurator->process($container);
}
return $container;
}
}
class DefaultServicesConfigurator implements IConfigurator
{
public $services = array(
'Nette\\Web\\IHttpRequest' => array(__CLASS__, 'createHttpRequest'),
'Nette\\Web\\IHttpResponse' => 'Nette\Http\Response',
// ...
);
public function process(IContainer $container)
{
foreach ($this->services as $name => $service) {
$container->addService($name, $service);
}
}
public static function createHttpRequest(IContainer $container)
{
// ...
}
// ...
}
class ConfigConfigurator implements IConfigurator
{
public function process(IContainer $container)
{
// vzasadě to samé co dělá stávající Nette\DI\Configurator::loadConfig()
}
}
Proč?
je jednoduché pak dělat něco jako „bundless“ v Symfony (třeba Doctrine viz: https://forum.nette.org/…te-framework)
Stávající Nette\DI\Configurator
je hodně
„neohebný“ – dělá moc různých věcí naráz.
Proč neposílám pull? Po předchozích zkušenostech se o implementaci ani(/zatím) nepokouším.
Editoval Patrik Votoček (9. 5. 2011 2:01)
- David Grudl
- Nette Core | 8218
Do prvního příspěvku jsem doplnil popis aliasování, kopírování a auto-wiringu.
- David Grudl
- Nette Core | 8218
Patrik Votoček napsal(a):
Proč neposílám pull? Po předchozích zkušenostech se o implementaci ani(/zatím) nepokouším.
Zatím to vážně nemá smysl, mám zatím jasnou představu, kam směřuji.
- Jan Tvrdík
- Nette guru | 2595
Všiml jsem si, že přestože v phpDoc je vyžadováno rozhraní
IContainer
, tak v praxi je na hodně místech potřeba
Container
kvůli metodám getParam
,
expand
, __get
a možná dalším. Elegantní řešení
mě ale nenapadá.
- Filip Procházka
- Moderator | 4668
Tohle jsem nepochopil? Já myslel, že to bude sjednoceno s params? Tohle vypadá jako registrace service
- Honza Marek
- Člen | 1664
Jan Tvrdík napsal(a):
Všiml jsem si, že přestože v phpDoc je vyžadováno rozhraní
IContainer
, tak v praxi je na hodně místech potřebaContainer
kvůli metodámgetParam
,expand
,__get
a možná dalším. Elegantní řešení mě ale nenapadá.
Přidat do IContainer getParam a setParam, getParam může vracet parametr expandovaný, __get není potřeba používat.
- David Grudl
- Nette Core | 8218
Jan Tvrdík napsal(a):
V praxi je na hodně místech potřeba
Container
kvůli metodámgetParam
,expand
,__get
Do rozhraní IContainer přidám práci s parametry, ale __get bych asi nechal jen součástí kontraktu. expand() by možná bylo nejvhodnější přetavit do statické funkce.
HosipLan napsal(a):
Já myslel, že to bude sjednoceno s params?
Je to v plánu, jen jdu po menších (zpětně kompatibilních) krůčcích.
Čelo napsal(a):
Je možné nějak jinak načíst dva oddělené configy?
…no a někdy to s tou kompatibilitou nevyjde :-) Bude to možné.
- Patrik Votoček
- Člen | 2221
Po pár hodinách pokusů a omylů tomu konečně začínám přicházet na kloub! A konečně chápu na co je víc kontejnerů.
Pár poznatků z „používání“:
- $context vs $container (nikdy si nevzpomenu jak se to zrovna tady na tom místě jmenuje)
- „
$app->context
“ je obrovsný BC break! (presenter máEnvironment::getContext()
a né$app->context
!!!) - u vlastního kontejneru nepoužívejte
createServiceFoo
jako výchozí služby (nepůjde to https://api.nette.org/…ner.php.html#56) - u defaultních služeb nejde registrovat
$typeHint
- hodně chybí tagy např.: panels (pro panely do debugbaru), run (pro autorun), atd…
- přidat prvek do pole které je jako param je opruz
Editoval Patrik Votoček (12. 5. 2011 0:04)
- David Grudl
- Nette Core | 8218
Patrik Votoček napsal(a):
- $context vs $container (nikdy si nevzpomenu jak se to zrovna tady na tom místě jmenuje)
$context je proměnná objektu, v níž má uložené služby, které pro svou činnost potřebuje (a shodou okolností je to taky DI\Container). $container je nějaký obecný DI\IContainer, používá se hlavně v Nette\DI.
- u vlastního kontejneru nepoužívejte
createServiceFoo
jako výchozí služby (nepůjde to …)- u defaultních služeb nejde registrovat
$typeHint
Přesnější je termín „statické“ kontejnery a statické služby, poté to dává smysl. Typehint jsem původně zvažoval brát z anotace @return, ale nezdá se mi to potřebné. Pokud pracuji se statickým kontejnerem, nepotřebuju kontrolovat typy jeho služeb, ty jsou dány.
- hodně chybí tagy např.: panels (pro panely do debugbaru), run (pro autorun), atd…
Budou.
- přidat prvek do pole které je jako param je opruz
Je, právě proto jsem taky měl $params jako public proměnnou.
Každopádně $cont->params['a']['b'] = 'c'
by fungovat mělo.
(mimochodem, zcela nezávisle na tom, jestli je kontejner zmražený nebo ne,
což uvádím v souvislosti s tímto
komentářem)
- David Grudl
- Nette Core | 8218
Kašlu na to, dám $params zase jako public a všechny metody pro práci s parametry vyhodím pryč.
- Patrik Votoček
- Člen | 2221
David Grudl napsal(a):
$context je proměnná objektu, v níž má uložené služby, které pro svou činnost potřebuje (a shodou okolností je to taky DI\Container). $container je nějaký obecný DI\IContainer, používá se hlavně v Nette\DI.
Tohle vím a chápu jen s tím v reálu poněkud bojuju.
- hodně chybí tagy např.: panels (pro panely do debugbaru), run (pro autorun), atd…
Budou.
Super!
- přidat prvek do pole které je jako param je opruz
Je, právě proto jsem taky měl $params jako public proměnnou. Každopádně
$cont->params['a']['b'] = 'c'
by fungovat mělo. (mimochodem, zcela nezávisle na tom, jestli je kontejner zmražený nebo ne, což uvádím v souvislosti s tímto komentářem)
Kašlu na to, dám $params zase jako public a všechny metody vyhodím pryč.
Proč vlastně params není Nette\ArrayHash
?
- David Grudl
- Nette Core | 8218
Patrik Votoček napsal(a):
Po pár hodinách pokusů a omylů tomu konečně začínám přicházet na kloub!
Každopádně jsem fakt rád, už jsem si myslel, že tady existuje nějaká mentální bariéra v chápání DI :-)
- Patrik Votoček
- Člen | 2221
Mě dělalo největší problém pochopit smysl / význam více kontextů pak už to šlo celkem snadno.
Dalším problémem bylo to že jsem studoval a nějaký čas i používal Symfony 2 DI. Kde je to „kanón na vrabce“ (aby se v tom dalo udělat všechno).
Obecně to je důvod proč mám Nette rád víc než Symfony 2 / Zend 2. Symfon 2 / Zend 2 jsou na úkor své (místy až ultimátní) univerzálnosti neskutečně ukecané.
- Patrik Votoček
- Člen | 2221
yep stejně jako je to teď u $form->values->foo
Editoval Patrik Votoček (14. 5. 2011 18:21)
- Cifro
- Člen | 245
Ako sa tá nová DI sranda používa celkovo v apikácii? Registrácia nových služieb v configu s rôznymi parametrami a prístup k ním. Chcelo by to update sandboxu (napr. službu pre pripojenie do DB).
A je možné používať Auto-wiring aj v config.neon? Mne sa pačilo ako to mal Vrtak v Nelle a v configu
Update:
Patrik Votoček napsal(a):
Mě dělalo největší problém pochopit smysl / význam více kontextů pak už to šlo celkem snadno.
Celkom by som bol rád keby si vysvetlil ten význam viacerých kontainerov. Ja to nepoberám :(
Editoval Cifro (15. 5. 2011 17:26)
- Filip Procházka
- Moderator | 4668
$context
znamená, že jde o kontext (hledej v českém
slovníku pojmů) dané služby. Když máš application, tak jejím kontextem
je Http\Request
, Http\Response
, IRouter
a
ještě pár dalších. Všechno ostatní jsou služby, pro tvou aplikaci.
- Z pomocí configu a výchozích
služeb, se vytvoří „hlavní“
Container
, který obsahuje všechny registrované služby. - Z toho
Container
u se pak na některé služby vytvoří reference v „kontextu“Application
, protožeApplication
, jiné služby nepotřebuje. - Reference na „hlavní“
Container
se pak přenese, pomocíPresenterFactory
, doPresenter
u a ty si v něm můžeš vesele štourat ve všech službách z configu atd.
Stejný princip se použije ještě někde, ale nechce se mi to dohledávat.
Když je nějaké služba, která nepotřebuje (čti nesmí) vidět všechny
služby, tak je vhodnější vytvořit novou instanci Container
u,
předat do ní jen pár služeb a tu pak předat jako „kontext“ dané
službě.
Kdybych mlel z cesty opravte mě někdo :)
Editoval HosipLan (15. 5. 2011 17:27)