Presentery: property lazy-autowire na steroidech

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip Procházka
Moderator | 4668
+
0
-

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

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

@enumag: absolutně nesouhlasím. Jediné místo, kde tahle praktika dává smysl a je odpustitelná jsou presentery.

enumag
Člen | 2118
+
0
-

@HosipLan: Možná máš pravdu. :-) Ale stejně bych rád slyšel důvod proč je to odpustitelné pouze v presenterech a jinde ne.

Editoval enumag (25. 4. 2013 13:59)

Filip Procházka
Moderator | 4668
+
+6
-

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__);
}

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

@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ší.

Filip Procházka
Moderator | 4668
+
0
-

Díky Honzo, Patriku za komentáře (přibyly opravné commity).

maryo
Člen | 15
+
0
-

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)

enumag
Člen | 2118
+
0
-

@HosipLan: Velmi oceňuji podrobné vysvětlení, vyvedls mne z omylu. :-) Teď už konečně chápu proč se inject* metody ve 2.0 automaticky volaly jen na presenterech a ne na službách. Zase jsem o něco chytřejší, díky!

Jan Tvrdík
Nette guru | 2595
+
0
-

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

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

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

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

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

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

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

Šaman
Člen | 2632
+
0
-

Mě se ten Noxův návrh líbí. Injectování čisté, minimum psaní a magie v rámci tolerance. +1

Nox
Člen | 378
+
0
-

@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)

Šaman
Člen | 2632
+
0
-

Tohle má nevýhodu v tom, že v dokumentaci nebude závislost zřejmá (private property do dokumentace nepatří.)

Nox
Člen | 378
+
0
-

Ono už settery na povinné závislosti je taková vratká věc imho, ale máš pravdu, toto je na tom ještě hůř. Tak pokus byl :)

Editoval Nox (18. 12. 2012 12:21)

hrach
Člen | 1834
+
0
-

Moc se mi to libi! To chci!

Filip Procházka
Moderator | 4668
+
0
-

@hrach: které? tohle? :))

Jan Tvrdík
Nette guru | 2595
+
0
-

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

hrach
Člen | 1834
+
0
-

@HosipLan: jj :)

Honza Marek
Člen | 1664
+
0
-

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

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

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

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

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.

hrach
Člen | 1834
+
0
-

Chcu tez trait! Presne jak psal JT – kvuli jednoduche znovupouzitelnosti.

pekelnik
Člen | 462
+
0
-

Ja chci taky trait :)

Editoval pekelnik (19. 12. 2012 12:04)

Filip Procházka
Moderator | 4668
+
0
-

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

@Šaman – máš pravdu, založím na to téma samostatné vlákno až budu mít funkční proof of concept.

maryo
Člen | 15
+
0
-

Jen detail. Když už, proč ne možnost nastřelit třeba blog.articles? Proč jen autowiring podle typu?

David Ďurika
Člen | 328
+
0
-

@maryo tu sa to uz riesilo…

Filip Procházka
Moderator | 4668
+
0
-

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

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

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

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

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

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

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

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

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

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

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

Více k tématu parametry v presenteru zde

Felix
Nette Core | 1183
+
0
-

Mozna trochu OF, jsem pro zavedeni anotaci co mozna nejvic. Kdyz se mrknu na Javu EE, tam je to jedna velka anotace. Navic kdyz mame tak skvely RobotLoader :-) Je pravdou ale, ze to neni pro zacatecniky tak pruhledne no.

Filip Procházka
Moderator | 4668
+
0
-

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.