Jak správně vytvářet Componenty přes context?

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

Stále mi moc nedochazí, jak mohu správně vytvořit componentu přez di. Základem je to, že chci vytvořit novou instanci nějaké třídy dedící z \Nette\Application\UI\Control a předat ji do konstruktoru jako první parametr presenter (to je pokud se nemýlím nutné ke správnému fungování komponenty). Jenže jak mohu předat presenter, když mi ji vytváří factory? Parametr do factory strčit nejde, nebo ano? Zůstávám tedy u toho, že si vytvářím komponentu v Presenteru slovem new NazevKomponenty, což je dle mého názoru taky špatně. Jak je to tedy správně? Díky

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Ono s tím předáním presenteru je to takto: jakmile továrnička (protected function createComponentFoo...) vrátí instanci komponenty, tak se na jejím parent prvku (instanci IContainer, což jsou kupříkladu presentery) zavolá ->addComponent($component, $name) – kde $component je právě instance té mojí komponenty. Tam se provede předání presenteru komponentě. Děje se tak tedy automaticky po vykonání továrničky. Viz zdroják

Stručně řečeno: v současně době komponentám není potřeba předávat v konstruktoru presenter ani žádný jiný parent prvek (prostě $this).

protected function createComponentFoo()
{
	return new FooControl;
}

Tohle je naprosto OK továrnička a FooControl bude mít k dispozici presenter (ne ale pochopitelně v konstruktoru, ten se volá (zcela logicky a očividně :) ) v továrničce – připojení k presenteru se odehraje až po jejím vykonání.

Je to dostatečně srozumitelné?

Update: ještě stojí za doplnění, že připojení k presenteru lze přímo detekovat pomocí páru metod monitor a attached. Tu první ani není třeba volat, jistojistý okamžik připojení k presenteru pak lze odchytit v attached:

protected function attached($parent)
{
	parent::attached($parent);
	if ($parent instanceof Nette\Application\UI\Presenter) {
		// do staff
		$this->presenter === $parent; // platí
	}
}

Editoval vojtech.dobes (16. 3. 2012 5:21)

joseff
Člen | 233
+
0
-

Díky za vysvětlení, ale já bych se právě raději vyhnul tomu slovu new ve vytváření Komponenty a raději bych komponenty vytvářel z contextu továrničkou, tedy

protected function createComponentFoo()
{
        return $this->context->createFooControl();
}

Což mi přijde mnohem čistější řešení, ale pak se presenter nepřipojí. Když pak tedy v komponentě zavolám $this->link(…) zařve to, že není připojený presenter. Takže teda jak?

Editoval joseff (16. 3. 2012 9:40)

Tharos
Člen | 1030
+
0
-

joseff napsal(a):

Což mi přijde mnohem čistější řešení, ale…

Moc by mě zajímala Tvá odpověď na mé následující otázky, ale než je položím… Neber je prosím jako nějaký útok nebo snahu o flame! Ber to třeba jen jako můj soukromý průzkum. :)

V čem ti tohle přijde mnohem čistější? Jaké to má pro Tebe praktické výhody oproti volání new? Je něco nečistého na konstrukci new?

Nesnažím se vůbec říct, že by mi Tvé řešení přišlo „špatné“ nebo tak něco. Určitě mají oba přístupy své výhody a nevýhody (výhodou je určitě možnost autowiringu) a opravdu by mě jen zajímalo, jaké má to či ono řešení výhody/nevýhody přímo pro Tebe a z čeho plyne to označení čisté/nečisté. Klidně ale můj post ignoruj. :) Díky!

joseff
Člen | 233
+
0
-

Vpohodě, nejsem taková citlivka :-)
Většinou do komponenty vkládám ještě mnoho dalších závislostí, např. translator, wwwDir, basePath, cache atd… a prostě mi přijde čistější napsat do configu:

class: ComponentFoo
arguments: [%wwwDir%, @translator, %basePath%]

než toto:

protected function createComponentFoo()
{
        return $this->context->createFooControl($this->context->parameters['wwwDir'], $this->context->translator, $this->context->parameters['basePath']);
}

Co když tuto komponentu vytvářím na více místech a v budoucnu se rozhodnu že tam přidám cache? Pokud to mám vše v configu jen přidám do configu do arguments cache a trochu překopu samotnou komponentu. Ale pokud to budu vždy předávat v prezenteru budu muset najít všechny výskyty createComponentComponentFoo a všude to přepsat. Nehledě na to že samotný zápis v presenteru je dost dlouhý a budu ho stále opakovat (kopírovat). No a nakonec bych chtěl mít všechny závislosti pěkně pohromadě. Pokud někde píšu blbost, budu rád když mi to někdo vysvětlí…

Editoval joseff (16. 3. 2012 10:42)

Tharos
Člen | 1030
+
0
-

Super, díky! Máš to dobře promyšlené a dává to smysl :). To opakované vytváření by se asi dalo vyřešit i vyčleněním té továrničky na úroveň nějakého společného předka, ale to se možná shodneme, že je méně elegantní řešení (minimálně pro mě by pak bylo hůře čitelné, kde že se ta komponenta vlastně bere).

joseff
Člen | 233
+
0
-

Mě by asi nejvíce uspokojilo, kdyby se do továrničky dal předat parametr, tedy

protected function createComponentFoo()
{
        return $this->context->createFooControl($this);
}

a továnička by pak dala $this (tedy presenter) do konstuktoru a ostatní zvyslosti za to, ale to nevím jak toho dosáhnout. Druhou možností je to udělat takto:

protected function createComponentFoo()
{
        return $this->context->createFooControl()->setPresenter($this);
}

Ale žádnou metodu setPresenter jsem v controlu nenašel :-(
Výsledek je tedy že vlasně nevím jak to mám vytvářet…

Filip Procházka
Moderator | 4668
+
0
-

Něco ke čtení. A teď konkrétně, mělo by to jít nějak takto.

factories:
	class: ComponentFoo
	arguments: [%presenter%, %wwwDir%, @translator, %basePath%]
	paremeters: [presenter]

Ale opět se musím vrátit na začátek tématu. Nemyslím si, že presenter během vytváření komponenty potřebuješ. Komponenta se totiž buď vykresluje, nebo zpracovává signál.

Když zpracovává signál, signál je volán presenterem. Je volán až v momentě, když už je komponenta dávno připojena a má přístupný $this->presenter.

Když komponentu vykresluješ, děláš tak v šabloně a předtím než komponentu vykreslíš, tak se musí vytvořit v továrničce, tedy opět během zpracování metody render máš už $this->presenter přístupný.

Pokud potřebuješ presenter dříve, což je oprávněný požadavek, přenes tuto logiku do metody attached, jak radí Vojta.

Tedy místo

public function __construct($parent, $name)
{
	parent::__construct($parent, $name);
	$this->presenter->link(); // což je stejně hovadina, proč bys tady potřeboval odkaz?
}

budeš psát

protected function attached($obj)
{
	parent::attached($obj);

	if (!$obj instanceof Nette\Application\UI\Presenter) {
		return;
	}

	// sem se zpracování dostane ihned po připojení k presenteru
	// zde máš už tedy presenter přístupný
	$this->presenter->link();
}

Je to daleko čistější řešení a více ComponentModel-friendly. Něco ke čtení.

joseff
Člen | 233
+
0
-

Super, díky moc za vysvětlení, je mi to o hodně jasnější! Dík

David Grudl
Nette Core | 8171
+
0
-

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

joseff
Člen | 233
+
0
-

HosipLan napsal(a):

Když komponentu vykresluješ, děláš tak v šabloně a předtím než komponentu vykreslíš, tak se musí vytvořit v továrničce, tedy opět během zpracování metody render máš už $this->presenter přístupný.

Tak asi něco dělám blbě, ale tohle mi nefunguje:
V Prezenteru:

protected function createComponentFoo(){
   return $this->context->fooControl;
}

V sablone controlu:

<a n:href=":Homepage:default">domu</a>

Chyba: Component or subcomponent name must not be empty string.
// nechapu jak to mam nastavit

Nebo toto:

<a href="{plink :Homepage:default}">domu</a>

Chyba: Call to a member function link() on a non-object
// Z cehoz podle me vyplyva ze tam presenter pripojeny neni
Pokud v sablone nepouzivam link, tak se vse vypise OK.

joseff
Člen | 233
+
0
-

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

Ale pokud používám new FooControl v presenteru tak DI porušuji také nebo ne? Takže jak na to správně přes DI?

Jendaaa
Člen | 21
+
0
-

joseff napsal(a):

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

Ale pokud používám new FooControl v presenteru tak DI porušuji také nebo ne? Takže jak na to správně přes DI?

Správně by si asi presenter neměl sahat do contextu pro komponenty, ale měly by my být předány konstruktorem, popř. setterem…

Editoval Jendaaa (21. 3. 2012 9:23)

ViPEr*CZ*
Člen | 814
+
0
-

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

To je takhle špatně? Nebo jsem nerozuměl odpovědi?

public function createComponentAddressBookFormAddFull() {
	$form = new MyForm($this->context->database);
	return $form;
}
llook
Člen | 407
+
0
-

ViPErCZ napsal(a):

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

To je takhle špatně? Nebo jsem nerozuměl odpovědi?

public function createComponentAddressBookFormAddFull() {
	$form = new MyForm($this->context->database);
	return $form;
}

To není špatně, ale také to není DI.

Ty ses ptal, jak to dělat správně podle DI a ten vzor popisuje řešení, kde objekt dostává všechny své závislosti zvenčí. Takže kdybys mermomocí chtěl použít DI, tak bys komponenty musel vytvářet někde „venku“ a do presenteru je injektovat. Na vytváření komponent se prostě DI nehodí.

ViPEr*CZ*
Člen | 814
+
0
-

Jasný, už jsem se lekl. Jedině možná použít metodu $this->getService(„database“); namísto magie, abych „nepoužil“ context. By mohlo být ještě hezčí…

mkoubik
Člen | 728
+
0
-

Z důvodu zpětné kompatibility je contejner naprosto legitimní závislost presenteru. Jako kompromisní řešení se mi líbí si někam uložit třeba $this->componentsFactory = $this->context->components aby továrničky nesahaly na celý context, ale jenom na tu část kterou potřebujou. V budoucnu by se to dalo vyčlenit a předávat do presenteru zvlášť.

mkoubik
Člen | 728
+
0
-

ViPErCZ napsal(a):

Nejlepší by samozřejmě bylo něco jako

/** @autowire */
public function setDatatbase(Connection $db)
{
    $this->database = $db;
}
Ascaria
Člen | 187
+
0
-

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

Ale jen v případě komponenty, v presenteru můžu používat kontext a zároveň neporušovat DI, nebo ne? Nenapadá mě způsob, jak bez použití contextu v presenteru předat komponentě nějaký model co se vytváří v neonu.

Jendaaa napsal(a):

joseff napsal(a):

David Grudl napsal(a):

Jen pro jistotu dodávám, že Dependency Injection je opakem contextu, tudíž používám-li context, porušuji a obcházím DI.

Ale pokud používám new FooControl v presenteru tak DI porušuji také nebo ne? Takže jak na to správně přes DI?

Správně by si asi presenter neměl sahat do contextu pro komponenty, ale měly by my být předány konstruktorem, popř. setterem…

Podle mě by presenter také neměl sahat pro komponenty do kontextu, měl by je vytvářet. Do kontextu patří modely a ne komponenty.

Editoval Ascaria (21. 3. 2012 19:55)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Ascaria napsal(a):

Ale jen v případě komponenty, v presenteru můžu používat kontext a zároveň neporušovat DI, nebo ne? Nenapadá mě způsob, jak bez použití contextu v presenteru předat komponentě nějaký model co se vytváří v neonu.

Proti DI je právě $context, tedy pattern zvaný Service Locator. Stačí si položit otázku, zdali presenteru předávám všechny závislosti. Odkud si jako bere ten model? Předávám ho presenteru v konstruktoru nebo setteru? Ne – DI dostává na frak.

Ascaria napsal(a):

Podle mě by presenter také neměl sahat pro komponenty do kontextu, měl by je vytvářet. Do kontextu patří modely a ne komponenty.

Kontext není knihovnička pro modelovou část aplikace, je to DI kontejner. V ideálním světě by v presenteru být neměl, protože všechno se předá čistě konstruktorem nebo setterem, nikoliv odkudsi z klobouku :).