Dependency Injection – Symfony inspired & Nella implementation – porting to Nette
- Patrik Votoček
- Člen | 2221
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 doNette\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
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
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
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
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
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
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
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
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
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
kravčo napsal(a):
Pri pohľade na
addParameter()
aaddFunctionCall()
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)
- Honza Marek
- Člen | 1664
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
run bol zjavne blbý príklad… upravil som pôvodný príspevok
Editoval kravčo (26. 4. 2011 13:19)