[2011–05–05] Finalizace Dependency Injection

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Grudl
Nette Core | 8136
+
0
-

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
+
0
-

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 | 8136
+
0
-

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
+
0
-

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
+
0
-

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 bude default.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
+
0
-

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
+
0
-

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.

Filip Procházka
Moderator | 4668
+
0
-

Compilery jsem myslel tak, jak to ma Patrik

vk83
Člen | 22
+
0
-

Na úvod se omlouvám za hloupou otázku. Ale dost často se tu píše o Dependency Injection, ale co to je?
Mohl by mi někdo nějak jednoduše vysvětlit, co to znamená Dependency Injection a jakou to má východu?

Tharos
Člen | 1030
+
0
-

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
+
0
-

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 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. :)

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
+
0
-

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
+
0
-

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.

Ondřej Mirtes
Člen | 1536
+
0
-

Doufám, že Environment se jednou úplně smaže.

bene
Člen | 82
+
0
-

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…

arron
Člen | 464
+
0
-

Ja jsem ten kod zatim nezkoumal moc podrobne, ale ono uz nejde mit sluzby v configu??

Editoval arron (5. 5. 2011 20:08)

Patrik Votoček
Člen | 2221
+
0
-

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 | 8136
+
0
-

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
+
0
-

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 | 8136
+
0
-

//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 | 8136
+
0
-

Tharos napsal(a):

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.

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 | 8136
+
0
-

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
+
0
-

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 | 8136
+
0
-

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.

Patrik Votoček
Člen | 2221
+
0
-

Ano to ano… Ale né pokud takto chci všechny „class“ služby.

Filip Procházka
Moderator | 4668
+
0
-

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
+
0
-

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
+
0
-

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 | 8136
+
0
-

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
+
0
-

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
+
0
-

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 | 8136
+
0
-

Do prvního příspěvku jsem doplnil popis aliasování, kopírování a auto-wiringu.

David Grudl
Nette Core | 8136
+
0
-

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
+
0
-

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
+
0
-

Tohle jsem nepochopil? Já myslel, že to bude sjednoceno s params? Tohle vypadá jako registrace service

Honza Marek
Člen | 1664
+
0
-

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řeba Container kvůli metodám getParam, 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.

Čelo
Člen | 42
+
0
-

zdravím,
dříve to šlo (do verzí z minulého týdne), ale v aktuální vývojové verzi je možné zavolat loadConfig v bootstrapu jen jednou. Při druhém zavolání to zahlásí, že config již byl načten.
Je možné nějak jinak načíst dva oddělené configy?

David Grudl
Nette Core | 8136
+
0
-

Jan Tvrdík napsal(a):

V praxi je na hodně místech potřeba Container kvůli metodám getParam, 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
+
0
-

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->contextje 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 | 8136
+
0
-

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 | 8136
+
0
-

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
+
0
-

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 | 8136
+
0
-

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
+
0
-

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é.

PetrP
Člen | 587
+
0
-

Patrik Votoček napsal(a):

Proč vlastně params není Nette\ArrayHash ?

Ze by se k params pristupovalo jako k property? $cont->params->foo

Patrik Votoček
Člen | 2221
+
0
-

yep stejně jako je to teď u $form->values->foo

Editoval Patrik Votoček (14. 5. 2011 18:21)

Cifro
Člen | 245
+
0
-

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
+
0
-

$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 Containeru se pak na některé služby vytvoří reference v „kontextu“ Application, protože Application, jiné služby nepotřebuje.
  • Reference na „hlavní“ Container se pak přenese, pomocí PresenterFactory, do Presenteru 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 Containeru, 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)