Presentery: property lazy-autowire na steroidech
- Filip Procházka
- Moderator | 4668
Hola,
dneska jsem byl línej psát inject*() metody a tak jsem si zkopíroval kód z Davidova článku a napsal jeden takovej šikovnej helper …
Tohle není doporučovaný způsob psaní presenterů!
… ale sakra pohodlnej! (Dirty-pleasure)
Jak to funguje?
No napíšu si presenter, který
- bude mít svoje properties
protected
- budou mít ve
@var
jaký typ mají obsahovat - budou mít navíc annotaci
@autowire
- budou mít ve
class ArticlePresenter extends BasePresenter
{
/**
* @autowire
* @var App\ArticleRepository
*/
protected $articleRepository;
// ..
}
Teď se bude dít to, že když presenter vytvořím, tak pomocí
černé magie BasePresenter
převezme kontrolu nad
$articleRepository
a proměnná nebude vůbec nic obsahovat až
přesně do momentu, kdy na ni sáhnu, třeba takto
public function actionDefault($id)
{
$row = $this->articleRepository->find($id);
}
Díky černé magii se až v tento moment vytvoří
instance třídy App\ArticleRepository
, vloží se do proměnné a
od toho momentu je k dispozici. Není potřeba žádných
accessorů, protože se o lenost samotnou stará
BasePresenter
.
(disclaimer: podobný princip používají i kluci a holky ve SWATu – jenom tenhle rape provádí na úplně všech třídách)
Tohle není čistý způsob
A já mám trochu výčitky, že se mi to líbí. No, ale protože netestuji presentery, tak mě to trápí o něco méně.
Výhody jsou naprosto zřejmé
- minimum psaní
- lazy
- autowiring
- nejsou potřeba accessory
nevýhody také
- je to černá magie
- znásilňuje to Dependency Injection Container na „podřadný“ ServiceLocator
Prosil bych konstruktivní diskuzi bez výroků typu „proč to ještě není v Nette?“, nebo tolik oblíbené „fůůůj, ty si prase!“. Děkuji!
Dám si alespoň týden na reálné používání. Pokud se ve mně brzy neprobudí svědomí, tak si to asi nasadím na většinu projektů…
Pokud se rozhodnete tenhle přístup také vyzkoušet tak pouze presentery! Ať vás ani nenapadne to strkat do modelových tříd!
Instalace
V composer.json
vložíme závislost
"require": {
"kdyby/autowired": "@dev",
}
Použití
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
use \Kdyby\Autowired\AutowireProperties;
}
Pokud nemáte PHP 5.4, tak si do BasePresenter
zkopírujte
obsah traity
- enumag
- Člen | 2118
Osobně bych to napsal spíše jako trait, aby to šlo používat
i jinde než v presenterech (komponenty, modely, služby). Mám možnost
používat PHP 5.4, tak proč ji nevyužít (uživatel PHP 5.3 si kód traitu
do BasePresenteru snadno zkopíruje). EDIT:
@HosipLan mne vyvedl
z omylu, byl to velmi špatný nápad.
Zatím ale dost váhám zda se do toho pouštět, je to nechutně pohodlná čuňárna. :-D
P.S. Do Nette podobná magie imho nepatří (byť jsem dříve měl opačný názor).
Editoval enumag (16. 12. 2012 18:27)
- Filip Procházka
- Moderator | 4668
@enumag: absolutně nesouhlasím. Jediné místo, kde tahle praktika dává smysl a je odpustitelná jsou presentery.
- Filip Procházka
- Moderator | 4668
Rád vysvětlím :)
Trocha teorie
Jak dostat do objektu nějakou závislost?
- konstruktor
- metoda
- property
- magie
Vždy, když je to možné, tak bys měl použít konstruktor injection. Je to nejčistější možný způsob a měl bys jej v 99% případů preferovat.
Method injection je tu proto, že některé závislosti objektu nejsou povinné. Typickým případem může být cache – můžeš chtít občas do objektu cache zavést pomocí něčeho takovéhoto
public function setCacheStorage(Nette\Caching\IStorage $storage)
{
$this->cache = new Nette\Caching\Cache($storage, __CLASS__);
}
A property injection jsem snad ještě nikdy nepoužil. (Upozorňuji, že to co jsem implementoval není property injection, ale ServiceLocator na úrovni presenteru)
Tím že použiješ méně jasný způsob vyžádání závislostí zaděláváš si na problémy s používáním třídy, zhoršuješ čitelnost kódu a zhoršuješ čistotu kódu.
Tolik teorie.
Proč vůbec inject*()
metody vznikly?
Kvůli presenterům. Presentery jsou opuchlé hromádky kódu, které by potřebovaly začít chodit do fitka. Dělají toho moc a proto potřebují hromadu závislostí. Už samotný UI\Presenter má 6 závislostí a ty mu je potřebuješ nějak předat.
Přičti si k tomu minimálně jednu a průměrně 5 závislostí, co používá tvůj konkrétní presenter a začne ti docela přetékat.
Ale to by ještě nebylo to nejhorší. Když používáš moduly a máš hierarchii presenterů (a to u složitější aplikace většinou máš) tak ti vzniká konstruktor-hell. Kdybys chtěl psát presentery nejčitějším možným způsobem, vypadaly by takto.
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
private $foo;
public function __construct(Foo $foo)
{
parent::__construct();
$this->foo = $foo;
}
}
namespace FrontModule;
abstract class BasePresenter extends \BasePresenter
{
private $bar;
private $lipsum;
public function __construct(Foo $foo, Bar $bar, Lipsum $lipsum)
{
parent::__construct($foo);
$this->bar = $bar;
$this->lipsum = $lipsum;
}
}
namespace FrontModule;
final class ConcretePresenter extends BasePresenter
{
private $dolor;
public function __construct(Foo $foo, Bar $bar, Lipsum $lipsum, Dolor $dolor)
{
parent::__construct($foo, $bar, $lipsum);
$this->dolor = $dolor;
}
}
A teď si představ, že bys chtěl přidat nějakou závislosti do
\BasePresenter
, budeš muset upravovat desítky presenterů. Takže
takhle ne. Proto vznikly inject*() metody a jediným
důvodem jejich existence je řešení tohoto problému.
Proč nepoužívat inject*() metody v modelu a komponentách?
Protože to není potřeba!
Jak vypadá standardní modelová třída, pokud je neprasíš? Pár závislostí v konstruktoru, pár metod a to je vše.
Jak vypadá standardní komponenta, pokud ji nesprasíš? Jedna dvě závislosti v konstruktoru, render metoda, šablona, signály, … a to je vše.
Vidíš tam někde důvod používat inject*()
metody? Já
teda ne.
Proč to tedy do presenteru patří?
Protože mě nebaví psát inject*()
metody (jako asi nikoho :)
a to na to mám live template v IDE, díky kterému mám inject metodu napsanou
za dvě vteřiny.
Presenter může mít několik akcí a každá může využívat jenom
některé služby. Pokud bys chtěl psát presenter čistě, tak vždy všechno
presenteru musíš předat. Bezpodmínečně. I s kontruktory i s
inject*()
metodami.
Protože chceme všichni psát krásné čisté aplikace, tak se začalo diskutovat
o accessorech. Což by měla být třída, která dostane DI Container a
jméno služby. Tento accessor si předáš do presenteru a když chceš
službu, zavoláš nad ním metodu ->get()
. Vytváření služeb
je díky tomu lazy, ale stále píšeš čistý kód, protože využíváš
Dependency Injection. Má to jednu jedinou nevýhodu – upíšeš se.
Proto jsem si začal dnes pohrávat s tímto. Je to nečisté, je to černá magie, ale je to pohodlné. Ale stále – jediný důvod proč to dělám je, že presentery jsou opuchlé boule kódu.
Já se snažím aplikace psát tak, abych měl modely krásné a voňavé (bezpodmínečně konstruktor injection), komponenty krásné a voňavé (také). Presentery se také snažím psát co možná nejčistějším způsobem, ale mají určitá specifika, které nelze ignorovat.
Z toho důvodu mi přijde relativně OK tato praktika (pouze v presenterech!).
- Jan Tvrdík
- Nette guru | 2595
@HosipLan: Jako odpůrce psaní inject*
metod považuji už dlouhou dobu autowirování do properties s
@inject
anotacemi jako jediný akceptovatelný
způsob, jak nahradit používání $this->context
,
který je ve srovnání s inject*
metodami výrazně
praktičtější a produktivnější.
- maryo
- Člen | 15
Trocha inspirace ze SF2 bundlu. Já to teda nepoužívám, ale na ty controllery/presentery to může být celkem fajn no.
http://jmsyst.com/…/annotations
Editoval maryo (16. 12. 2012 18:15)
- Jan Tvrdík
- Nette guru | 2595
@HosipLan: Co takhle umožnit zápis
@autowire serviceName
? Řešilo by to problém, kdy nelze určit
službu pouze na základě typu.
- David Ďurika
- Člen | 328
@Jan Tvrdík +1
tak uz mi @hosiplan vysvetlil ze to nieje az tak dobry napad (porusuje to
inversion of control)
Editoval achtan (17. 12. 2012 10:22)
- Felix
- Nette Core | 1245
Jsem take pro zavedeni @autowired v konecnem dusledku to bude inject*, akorat pohodlnejsi.
Idealne kdyby to umelo akceptovat i interface viz https://forum.nette.org/…ept-generics
/**
* @autowire IMyService
*/
protected $myService;
<=>
/**
* @autowire MyServiceImpl
*/
protected $myServiceImpl;
- Filip Procházka
- Moderator | 4668
Felix napsal(a):
Idealne kdyby to umelo akceptovat i interface
Interface to přece umí taky – koukni na implementaci ;)
achtan napsal(a):
tak uz mi @hosiplan vysvetlil ze to nieje az tak dobry napad (porusuje to inversion of control)
Co si budem, tohle ho to taky porušuje :) Když už porušujeme pravidla, musíme vědět proč a jaká ;)
- Nox
- Člen | 378
Nemůžu dostat z hlavy jednu asi až moc očividnou věc – chceš řešit injection hell, ale vadí servicelocatior + magie … co to redukovat jen na magii (stejně černou a prohnilou, leč…)?
Místo aby se @autowire automaticky načetlo ala service locator, co takhle dát kód do __call který by simuloval injectXY? Takže mechanismus injectů by zůstal kompletně zachovaný, jen by už kód neplevelily injecty, ale jen anotace (podobně jako u Nette\Object::get by to šlo samozřejmě překrýt vlastní metodou, pokud by tam člověk chtěl ještě něco dělat)
Něco mi uniká?
Edit: aha, nebude to lazy … ale tak případně jako druhá možnost
Editoval Nox (17. 12. 2012 11:02)
- Filip Procházka
- Moderator | 4668
Injection hell, to slyším prvně :D
ServiceLocator mi vadí jak kde a magie mi nevadí (jak říkám: když víš přesně jak to funguje, tak to přestává být magie) – magie vadí obecně, kvůli začátečníkům, protože nebudou chápat, jak jsme toho chování dosáhli.
No a ten tvůj nápad jsem vůbec nepochopil ;)
- Jan Tvrdík
- Nette guru | 2595
@HosipLan: Pokusím se ti vysvětlit ten jeho nápad.
Místo
class MyPresenter extends UI\Presenter
{
/** @var ArticleRepository */
private $repo;
public function injectArticleRepository(ArticleRepository $repo)
{
$this->repo = $repo;
}
}
by se použil kód (tu @method
anotaci jsem doplnil já, jinak
netuším, jak by to mohlo fungovat):
/**
* @method injectArticleRepository(ArticleRepository $repo)
*/
class MyPresenter extends UI\Presenter
{
/**
* @var ArticleRepository
* @inject
*/
private $repo;
public function __call($method, $args)
{
// dark magic
}
}
Je zřejmé, že anotace nad private $repo
by bylo možné
vyhodit a řídit se jen @method
anotacemi.
Nápad pěkný, ale nelíbí se mi, ač skutečně nepracuje se service locatorem.
- Nox
- Člen | 378
@Jan Tvrdík: to by se muselo ještě domyslet, protože s tou @method je toho psaní tak asi stejně, ne méně, takže nevýhody spíš převáží. V method je duplicitně typ (už je ve @var), ohledně názvu – ten by se dal doplnit za @inject s tím, že by byla nějaká konvence, nejspíš podle názvu proměnné (pokud to v rámci anotací je možné) – a pak by se nemuselo psát ani tam. Pak by šlo @method odstranit. S tím, že by pak už nebylo v CC – jediný případ, kde by to vadilo, mě napadlo – pokud někdo dělá unit testy presenterů.
Pak by to bylo
<?php
class MyPresenter extends Presenter // featuring dark magic
{
/**
* @var ArticleRepository
* @inject
*/
private $articleRepository; // chybí serviceName = udělá inject.ucfirst($varname)
}
?>
Editoval Nox (17. 12. 2012 18:17)
- Jan Tvrdík
- Nette guru | 2595
@Nox: Tak tohle jsem nepobral ani já. Netuším, kde to má hlavu a kde patu, k čemu to je, jak to bude fungovat. Prostě nic.
S tím, že by pak už nebylo v CC
Většinou mi zkratky jdou, ale teď se nechytám.
- Honza Marek
- Člen | 1664
enumag napsal(a):
Osobně bych to napsal spíše jako trait, aby to…
Udělat to znovupoužitelně by bylo rozumné. Co když to bude chtít někdo jiný nebo někdo stejný v jiném projektu taky používat? Má kvůli tomu mačkat ctrl c a ctrl v?
- Filip Procházka
- Moderator | 4668
Je to účelně dělané tak, aby to bylo jen v BasePresenteru a tedy použitelné pouze v presenterech.
- Jan Tvrdík
- Nette guru | 2595
@HosipLan: To je pěkné, ale kdyby to byl(a?)
trait
, z toho můžeš udělat Composer balíček. Pokud ti tak
vadí používání jinde, tak tam přidej kontrolu přes
is_subclass_of
.
- LeonardoCA
- Člen | 296
@Hosiplan: Zkusím nahodit úplně jinou perspektivu. (mírně off topic) Injektování komponent jen v konstruktoru? Co když mám problém, který popisuješ posunutý na úroveň komponent a používám prezenter, jen proto že to zatím neumím jinak (jako prostředníka na generování komponent a vše ostatní řeším v komponentách)?
Důvod a vize: Co kdyby se komponenta mohla kdykoli sama stát „prezenterem“ a zároveň, aby mohlo být více "komponent(prezenterů)¨ na stránce nebo jeden a tentýž vícekrát? Dělení Prezenter ⇒ komponenty beru jako nepříjemně svazující. (mám už představu jak by to mohlo fungovat, ale ještě hodně abstraktní)
- Šaman
- Člen | 2662
To už je hodně offtopic, ale souhlasím, že by se mi to hodně líbilo. Už teď se snažím sjednotit přístup k presenerům a ke komponentám. A souhlasím i s tím, že presenter je příliš magický (má spousty nativních závislostí). Ale zase má-li být Nette vhodné i pro nováčky, tak nemůžeme po programátorovi chtít, aby si ručně předával requesty, routery a containery. Ledaže by to vyřešil sandbox, tedy plain (empty) project.
- Filip Procházka
- Moderator | 4668
Tak ok no, udělal jsem traitu :P
Davídku, co ty si o tom myslíš? Já vím že chceš tlačit striktní DI (alespon jako vychozi best practise), ale ty presentery jsou fakt oser :)
- LeonardoCA
- Člen | 296
@Šaman – máš pravdu, založím na to téma samostatné vlákno až budu mít funkční proof of concept.
- Filip Procházka
- Moderator | 4668
@maryo: protože přesouváš „konfiguraci“ do aplikace. Jména služeb jsou nepodstatná – důležité jsou interfacy a abstrakce. Proti nim máš programovat a ty máš vyžadovat. To že na interfacy v 90% případů kašleme, je jenom otázka lenosti.
Jsem přesvědčen, že tady ta lazy-autowire magie je pořád menší zlo, než vyžadovat služby podle názvu.
- maryo
- Člen | 15
Já v Nette nedělám (firemní rozhodnutí), i když na soukromej projekt bych si možná vybral, tak se můžu mýlit.
Pokud se používá BasePresenter, neregistrují se presentery jako služby, tak je ten presenter stejně závislej na containeru a používá se stylem service locatoru. Tj. že se stejně tahaj služby pomocí getService (nebo jak to v nette je) a jejich jména (pokud se nepoužije ta tvá magie), ne?. Pak už to vyjde nastejno, ne? Nebo mi něco nedochází (pravděpodobně :))?
Editoval maryo (21. 12. 2012 15:17)
- Jan Tvrdík
- Nette guru | 2595
@maryo:
Já v Nette nedělám (firemní rozhodnutí)
Čas jít pracovat jinam.
Pokud se používá BasePresenter, neregistrují se presentery jako služby, tak je ten presenter stejně závislej na containeru a používá se stylem service locatoru.
Bylo tomu tak, ale od určité doby už třída UI\Presenter
není na DI containeru závislá. Předává se pouze kvůli zpětné
kompatibilitě.
- maryo
- Člen | 15
Čas jít pracovat jinam.
Symfony2 taky neni zlý. Má větší komunitu a víc rozšíření (bundlů). Ale asi bych si stejně vybral Nette, nemám v tom ještě jasno kdo by zvítězil. Nicméně nechci rozjíždět flame a navíc je to tu trochu OT :)
Bylo tomu tak, ale od určité doby už třída
UI\Presenter
není na DI containeru závislá. Předává se pouze kvůli zpětné kompatibilitě.
Cool 8-)
BTW když předání služby na základě její třídy/interfacu stačit nebude (i když ve většině případech to asi stačí), co pak? Pár závislostí se bude muset konfigurovat jinak a to neni asi úplně ono nebo se vám to ještě nestalo? Nebo to je ono? :)
- Filip Procházka
- Moderator | 4668
Mně se to ještě nestalo, protože se snažím psát čistě, ale spousta lidí mi tady bude nejspíš odporovat, že s tím mají každou chvíli problémy ;)
Jediná poslední věc, kterou Nette ještě neumí jsou generické služby. Asi to brzy napíšu jako rozšíření. Je to příliš specifická věc a Davidovi se to nejspíš nebude chtít začlenit.
- maryo
- Člen | 15
Nevím proč by to nutně muselo být nečistý. Někdy prostě požaduješ nějakej abstraktní typ jako závislost (Interface, Abstraktní třídu) a na jednom místě chceš předat jednu implementaci a na druhym místě jinou (obě jsou registoravný jako služby v kontejneru). Tam už si asi jen s typem nevystačíš, ne? Ale žádnej rozumnej příklad pro Presenter mě teď zrovna nenapadá :). Pro obyčejný služby by se jich našlo asi dost.
(To s těma generickejma službama už jsem četl, zní to zajímavě)
Editoval maryo (21. 12. 2012 17:06)
- Filip Procházka
- Moderator | 4668
Já to řeším tak, že prostě vytvořím novou třídu, tu zaregistruju
v configu a tam snadno určím, která implementace se má předat (pomocí
@foo2
). Kód, který pracoval s touto službou přesunu
z presenteru do nově vytvořené třídy a tuto třídu už si pak bez
problému můžu vyžádat pomocí autowire :)
- Jan Tvrdík
- Nette guru | 2595
@HosipLan: Tohle obcházení problému mi přijde
výrazně horší, než povolení @autowire serviceName
, které
neporušuje nic víc, než to, co už je dávno porušeno celým konceptem
@autowire
anotací.
- Filip Procházka
- Moderator | 4668
No a já nesouhlasím a myslím si, že to není obcházení problému, ale
jeho řešení :) Protože stejně bych to řešil i bez
@autowire
.
- Jan Tvrdík
- Nette guru | 2595
Považuji to za obcházení problému zcela nezávisle na existenci
@autowire
. Ten problém prostě současná Nette implementace
(předpokládající nepoužití $this->context
) má a ty
nenabízíš řešení, pouze konstatuješ, že
workaround který existoval před @autowire
existuje a funguje pořád.
- maryo
- Člen | 15
HosipLan: Novou třídu? Máš jednu třídu, např. třídu BlaBla co vyžaduje něco jako ICache. Pak máš třeba ApcCache, ArrayCache, FileCache. A na jednom místě potřebuješ BlaBla s ApcCache a na druhym např. BlaBla s ArrayCache. Jakou novou třídu bys vytvářel? Taky mi to na první pohled přijde jako obcházení problému, Container by se měl přizpůsobit API, ne API containeru. Ale možná to jen špatně chápu a dělal bys to tak i bez něj. Nechceš ukázat příklad? :) Já si chci napsat DI container v Dartu a přemejšlim, jak toho docílit nejlíp a nejjednosušejc (akorát mě tam štve, že všechny knihovny asi musim importnout).
Mimochodem přijde mi špatný, že se ty závislosti zvenku předávaj do soukromejch vlastností a neni to nijak vidět v API. Alespoň by šlo přidat ještě anotaci @property-write. Anebo to udělat public a unsetnout (kvůli volání __set). A ještě by to chtělo nějak ošetřit, že jsou všechny povinný vlastnosti injectnutý.
Nějak tak, jak to psal DG tady https://phpfashion.com/…ty-injection
Editoval maryo (25. 12. 2012 15:02)
- Filip Procházka
- Moderator | 4668
Prosil bych všechny o migraci na kdyby/autowired. Původní balíček ani repozitář mazat zatím nebudu, ale nebude ani vyvíjen. Použití je naprosto identické, stačí změnit namespace v BasePresenteru.
Oproti původnímu obsahuje kdyby/autowired hromadu vylepšení a v repozitáři je navíc druhá traita pro továrničky na komponenty, od matej21. Více v nové dokumentaci.