Dependency Injection – Symfony inspired & Nella implementation – porting to Nette

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Patrik Votoček
Člen | 2221
+
0
-

Již poměrně dlouhou dobu používám(e) v Nella Frameworku pokročilejší implementaci Dependency Injection (dále DI). Nicméně toto DI bylo naroubované na stávajíci Nette a to bez jediného zásahu přímo do Nette. V tomto důsledku mělo k dokonalosti hodně daleko, kvůli nejrůznějším omezením (hlavně nutnosti implementovat Nette\DI\IContext a dědit Nette\DI\Configurator).

Poměrně dlouhou dobu mě v hlavě ležel nápad s pár vylepšeními a hlavně myšlenka portace do Nette. A tak jsem se do toho konečně pustil. Už je to rozdělané. Budu se snažit to průběžně házet na github (testovatelné řešení by mohlo být v průběhu dneška – zatím je tam pracovní verze – budou i testy (až se mě je podaří odbugovat)!).

Implementace je inspirovaná Symfony 2. Několik měsíců v praxi testována a vylaďována v „ostrém“ prostředí (pominuli několik málo změn).

Seznam nejdůležitějších změn a novinek

  • sloučení config & variables (nyní už pouze jako variables)
  • přesun toho všeho do Nette\DI\Context
  • aliasy pro služby (jmenné)
  • lepší konfigurovatelnost služeb díky „univerzální“ továrničce na služby
  • Nette\DI\Configurator předělán na kompilátory + detekce prostředí přesunuta do Nette\Environment

Jak to celé funguje

Environment::loadConfig(); spustí Nette\DI\ContextBuilder který má za úkol vytvořit a naplnit Nette\DI\Context. Provede to za pomoci dvou kompilátorů a to Nette\DI\DefaultServicesCompilator (inicializace základních služeb) a Nette\DI\ConfigCompilator (zpracování config.neon / config.ini). Pokud pak chcete z Environmentu vytáhnout službu / proměnnou prostředí tak ve skutečnosti voláte context.

Změny samotného Nette\DI\Context

Pro celkově lepší práci se službami vznikl contextův pomocník Nette\DI\ServiceFactory. Ten má nastarosti právě vytváření služeb a držení informací o tom jak to má dělat.

Praxe

Simple class

PHP:

$context->addService('App\MyService', 'App\MyServiceClass');

Neon:

common:
	services:
		App\MyService = App\MyServiceClass
		#nebo
		App\MyService:
			class = App\MyServiceClass
Simple factory

PHP:

class MyService
{
	function create()
	{
		return new self;
	}
}

$context->addService('App\MyService', 'App\MyService::create');
//nebo
$context->addService('App\MyService', array('App\MyService', 'create'));
//nebo
$context->addService('App\MyService', callback('App\MyService', 'create'));
//nebo
$context->addService('App\MyService', function() {
	return new App\MyService;
});

Neon:

common:
	services:
		App\MyService = App\MyServiceClass::create
		#nebo
		App\MyService:
			factory = App\MyServiceClass::create
Class s parametry konstruktoru

PHP:

$context->addService('App\MyService', 'App\MyServiceClass')->setArguments(array('foo', 'bar'));
//nebo
$context->addService('App\MyService', 'App\MyServiceClass')->addArgument('foo')->addArgument('bar');

NEON:

common:
	services:
		App\MyService:
			class = App\MyServiceClass
			arguments = [foo,bar]
Továrnička s parametry

PHP:

$context->addService('App\MyService', function($first, $second) {
	return new App\MyService($first, $second);
})->setArguments(array('foo', 'bar'));
//nebo
$context->addService('App\MyService', function($first, $second) {
	return new App\MyService($first, $second);
})->addArgument('foo')->addArgument('bar');

NEON:

common:
	services:
		App\MyService:
			factory = App\MyServiceClass::create # function create($first, $second) { ... }
			arguments = [foo,bar]
Vstřikování pomocí metod

PHP:

$context->addService('App\MyService', 'App\MyServiceClass')->addMethodCall('setFoo', array('foo', 'bar');
// provede toto:
$test = new App\MyServiceClass;
$test->setFoo('foo', 'bar');
// tj je ekvivalentem pro
$context->addService('App\MyService', function() {
	$test = new App\MyServiceClass;
	$test->setFoo('foo', 'bar');
	return $test;
});

NEON:

common:
	services:
		App\MyService:
			factory = App\MyServiceClass
			methods:
				- {method: setFoo, arguments: ['foo','bar']}

Metod můžeme volat samozřejmě kolik chceme.

Konfigurátor

PHP:

$context->addService('App\MyService', 'App\MyServiceClass')->setConfigurator(function($service) {
	$service->setFoo('foo', 'bar');
	return $service;
});

NEON:

common:
	services:
		App\MyService:
			class = App\MyServiceClass
			configurator = App\MyServiceClass::configurate # function($service) { ... }

Specialitky argumentů:

Argumenty konstruktorů, továrniček a metod nemusejí být jenom skalární data ale i něco lepšího.

Environment variable
common:
	services:
		App\MyService:
			class = App\MyServiceClass
			arguments = ['%foo%'] # %foo% bude nahrazeno za "test"
	foo = "test"

Jednoduže řečeno cokoli mezi dvěma znaky % bude nahrazeno za data z proměnné prostředí se stejným názvem.

Service
common:
	services:
		App\MyService:
			class = App\MyServiceClass
			arguments = ['@Nette\Caching\IStorage'] # bude nahrazeno za instanci cache úložiště získaného pomocí služeb

Argument začínající zavináčem bude nahrazen za instanci služby se stejnám názvem.

Editoval Patrik Votoček (25. 4. 2011 1:43)

Filip Procházka
Moderator | 4668
+
0
-

Subjektivní názory z používání upravené Patrikovy implementace:

Lepší argumenty

Jsem pro doplnit ještě možnost, zadat klíč assoc pole, jako je vidět v konfiguraci: https://github.com/…actories.php#L11

třeba

'arguments' => array('%Application[application.class]%'),
'arguments' => array('%memcache[host]%', '%memcache[port]%', '%memcache[prefix]%'),

Výhody jsou zcela zřejmé, není potřeba dělat factory, jenom proto, aby se předaly jednoduché argumenty třídě.

Mám to implementováno, sice to není možné nejefektivnější, ale funguje to podle očekávání a spolehlivě.

Taky @internal metoda expandParameter, podle tvého původního návrhu se mi moc nelíbila. Tak jsem ji rozdělil a upravil tak, jak podle mě dává smysl.

Config a proměnné prostředí

Vyloženě se nabízí implementace ArrayAccess pro ServiceContainer pro přístup k těmto proměnným, jako zkratka

$context->getParameter('database');
$context['database'];

Aliasy

Myslím si, že implementace aliasů, vytvářením odkazů v rámci Context, jenom pod jiným názvem je zbytečné. Naopak bych aliasy implementoval jenom jako zkratku, která bude přístupná přes magický __get()

Má to naprosto zřejmou výhodu, že pokud se doplní phpDoc (alespoň defaultních) služeb. Práce s Contextem je pak vyloženě radost.

$this->context->getService('Doctrine\\ORM\\EntityManager'); # našeptávač v IDE ani neškytne
$this->context->entityManager; # našeptávač ví, že má napovídat metody třídy `EntityManager`

Prosím,

rád se nechám přesvědčit o užitečnosti jedné instance služby pod dvěma názvy

$this->context->getService('Doctrine\\ORM\\EntityManager');
$this->context->getService('entityManager');

A taky bych chtěl vědět co se ti nelíbí, na ArrayAccesu pro proměnné (afaik jsi to měl jako zkratku pro služby a jejich aliasy)

PS: Skoro se bojím i napsat, že Symfony to má tak, jak jsem si to upravil já (alespoň ty proměnné určitě), protože vždycky když to někdo napíše, tak David začne stávkovat :)

Filip Procházka
Moderator | 4668
+
0
-

Když teď na to koukám, tak je vidět, že mají význam ty aliasy, kvůli zpětné kompatibilitě. To ale nebrání mít ServiceAlias a k tomu ještě zkratku přes magický __get().

Ondřej Mirtes
Člen | 1536
+
0
-

HosipLan: Nesouhlasím s ArrayAccess (je to WTF).

Líbí se mi, co Honza Marek udělal u nás v Mediu – sestavuje pomocí DI kontejneru i presentery. Do Presenterů tedy není vstřikován kontejner, ale už konkrétní servisy, které daný presenter potřebuje. Chce to pouze vlastní jednoduchou implementaci PresenterFactory.

Samotný kontejner je pak přístupný pouze na pár speciálních místech – v bootstrapu, v testech a v cron skriptech. Zbytek aplikace o něm neví.

Filip Procházka
Moderator | 4668
+
0
-

Co se týče těch zkratek, je mi to v podstatě fuk. Stejně si nad tím budu dělat vrstvu, abych tam tohle chování dostal. Ale ty aliasy a klíče v konfiguraci mi tam vyloženě chybí.

Ondřej Mirtes
Člen | 1536
+
0
-

Vždyť si svoje služby můžeš pojmenovávat jakkoli, nemusíš je mít pod dvěma různými názvy.

Patrik Votoček
Člen | 2221
+
0
-

Ad ArrayAccess není tam účelně (je to WTF). Nějak nechápu jak myslíš aliasy … v konfiguraci mi tam vyloženě chybí.

A ad %foo[bar]% spíš bych použil tečkovou notaci %foo.bar%

Editoval Patrik Votoček (25. 4. 2011 19:23)

kravčo
Člen | 721
+
0
-

Slovo „compilator“ zdá sa existuje, ale po anglicky/americky sa tomu vo vačšine nadáva „compiler“ :)


Namiesto zavináča by bolo oveľa prirodzenejšie použiť zrozumiteľné { service: Nette\Caching\ICacheStorage }, na druhej strane je to aj možný zápis pre (object) array('service' => '\Nette\Caching\ICacheStorage'), neviem či sa s týmto neráta…

… alebo žeby sa nám zavináčová mágia vrátila :)


Pri pohľade na addParameter() a addFunctionCall() rozmýšľam, … načo? Kložúry to zvládnu za nás, nie?

$context->addService('cache', function() {
    $storage = new \FW\CustomMemcachedStorage;
    $cache->foo = 'bar';
    $cache->expire('volatiles');
    return new \Nette\Cache($cache);
});

Nevidím prínos toho istého kódu v konfiguráku, ktorý mi pripadá o dosť ukecanejší a menej čitateľný a zrozumiteľný (je to môj subjektívny pocit)…

common:

    services:
	\FW\CustomMemcache:
            class: \FW\CustomMemcache
            setMembers: { foo: bar }
            callMethods: { expire: [ volatile ] }
        cache:
            class: \Nette\Cache
            args: [ { service: \FW\CustomMemcache } ]

A furt a stále je to len factory, len zapísaná iným spôsobom. Tak prečo jej nenechať flexibilitu PHP? Naozaj ma zaujímajú vaše dôvody.

Patrik Votoček
Člen | 2221
+
0
-

kravčo napsal(a):

A furt a stále je to len factory, len zapísaná iným spôsobom. Tak prečo jej nenechať flexibilitu PHP? Naozaj ma zaujímajú vaše dôvody.

Jde o to že nemusíš vůbec žádnou továrničku tvořit. (nemusíš zasahovat do kódu ) + dává ti to flexibilitu kdy chceš třeba v nějaké instanci své aplikace něco malinko jinak nastavit.

Filip Procházka
Moderator | 4668
+
0
-

Hmm a jak tu tečku odlišíš od teček v konfiguraci pro php? Přece nechceš zavádět, že v jedné části configu by tečka byla součást názvu a v druhé části configu bude tečka znamenat cestu.

Filip Procházka
Moderator | 4668
+
0
-

kravčo napsal(a):

Pri pohľade na addParameter() a addFunctionCall() rozmýšľam, … načo? Kložúry to zvládnu za nás, nie?

A furt a stále je to len factory, len zapísaná iným spôsobom. Tak prečo jej nenechať flexibilitu PHP? Naozaj ma zaujímajú vaše dôvody.

Protože addService nemůžeš použít v configu. To chceš mít v bootstrap (v compileru) 100× addService?

// edit: po ránu pomalejší, jsem to mohl napsat do jednoho…

Editoval HosipLan (26. 4. 2011 7:51)

kravčo
Člen | 721
+
0
-

HosipLan napsal(a):

Protože addService nemůžeš použít v configu. To chceš mít v bootstrap (v compileru) 100× addService?

Nerozumiem. Toto predsa funguje, nie?

service.CustomCache.factory = "\FW\Factory::createCustomCache"

Editoval kravčo (26. 4. 2011 13:19)

Honza Marek
Člen | 1664
+
0
-

To run je taky docela šílená věc, kterou by bylo záhodno zakázat.

  • Vytvoření služby by nemělo ovlivňovat globální prostředí. Továrnička by mi jen měla poskytnout objekt, se kterým nějak naložím.
  • Další, co mi na run vadí je šílená jednoúčelovost. Přitom by se to dalo krásně zobecnit na nějaké tagování služeb, že bych si na jednom místě vytáhl služby otagované jako autoload a v cyklu je naházel do spl_autoload_register. Na jiném místě bych si zas vytáhl služby otagované jako helpery pro šablony třeba a registroval je v šabloně. Využití tagů je širší než mít k dispozici jen run.
kravčo
Člen | 721
+
0
-

run bol zjavne blbý príklad… upravil som pôvodný príspevok

Editoval kravčo (26. 4. 2011 13:19)