Jak zjišťujete, jestli komponenta půjde vytvořit?
- pata.kusik111
- Člen | 78
Ahoj,
snažím se vymyslet způsob, jak zjistit během běhu presenteru, jestli
komponenta půjde vytvořit a podle toho se rozhodnout, co případně
podniknout dál. Například, pokud vím, že se jedná o komponentu, která je
naprosto kritická pro vykreslení stránky a bez ní nemá stránka smysl, tak
třeba přesměrovat někam jinam. U takovýchto komponent nemám problém
v tom, že nebudou „lazy-loaded“ přes createComponent*
až
v šabloně, ale klidně si je instancuju už v Presenteru. (konec konců
u těchto „kritických“ komponent vím, že budou určitě vykreslené)
Jako ideální místo se mi proto tváří metoda action*
,
které má výhodu toho, že může pro různé metody kontrolovat různé
komponenty a protože v action se dá ještě přesměrovávat. Tohle mi zatím
zní super, přesně to, co hledám. Jenže pak jsem začal číst
dokumentaci:
Továrny nikdy nevoláme přímo, zavolají se samy ve chvíli, kdy komponentu poprvé použijeme. Díky tomu je komponenta vytvořena ve správný okamžik a pouze v případě, když je skutečně potřeba. Pokud komponentu nepoužijeme (třeba při AJAXovém požadavku, kdy se přenáší jen část stránky, nebo při cachování šablony), nevytvoří se vůbec a ušetříme výkon serveru.
Komponenty a ovládací prvky (důraz můj)
Z čehož jsem pochopil, že tohle řešení je pěkně na h***o v případě AJAXových požadavků, kde ty komponenty budu úplně zbytečně vytvářet i když budu překreslovat něco úplně jiného nebo třeba jenom zpracovávat nějaký signál.
Takže mě zajímá, jak tohle řešíte vy? Abych ještě podle toho, jestli
komponenta půjde vytvořit mohl přesměrovat, ale zároveň ji nevytvářel
pro ajaxové požadavky? Presenter::isAJAX()
? Nebo něco
jiného?
Background – co myslím tím, že komponenta nepůjde
vytvořit
V podstatě mi jde o získání parametrů pro konstruktor komponenty. Do něj
posílám zásadně jenom data(případně generator, když to chci lazy) a
veškeré úkony řeším přes callbacky v presenteru – v komponentách
nemám servisní třídy. Nicméně pro ty data z service si chci volat až
když je budu potřebovat a ne všude mám generátory, takže volat si o data
někdy v Presenter::startup()
, uložit do proměnné presenteru a
pak jen použít v createComponent*
se mi taky příčí.
- Kamil Valenta
- Člen | 820
Komponenta už nemá co přesměrovávat. Její životní cyklus je na
úrovni signálu a renderu.
Takže Tě asi nezbude nic jiného, než v action zjistit, zda máš data pro
komponenty, které se budou vykreslovat a pokud ne, tak přesměrovat. Pouze
isAjax() nestačí, protože můžeš ajaxem překreslovat právě některou
komponentu.
Při běžném requestu tedy musíš vytahovat z modelu data pro všechny
komponenty, při ajaxovém requestu musíš vytáhnout data právě
překreslovaných komponent.
- pata.kusik111
- Člen | 78
Kamil Valenta napsal(a):
Komponenta už nemá co přesměrovávat. Její životní cyklus je na úrovni signálu a renderu.
Takže Tě asi nezbude nic jiného, než v action zjistit, zda máš data pro komponenty, které se budou vykreslovat a pokud ne, tak přesměrovat. Pouze isAjax() nestačí, protože můžeš ajaxem překreslovat právě některou komponentu.
Při běžném requestu tedy musíš vytahovat z modelu data pro všechny komponenty, při ajaxovém requestu musíš vytáhnout data právě překreslovaných komponent.
Ano, v tom co říkáš se naprosto schodneme a v podstatě to samé jsem se i snažil napsat. Nicméně ta otázka zůstává v zásadě stejná. Jak načítat data pouze pro komponenty, co se budou vykreslovat/překreslovat(to jest při normálním requestu pro všechny, při subrequestu POUZE A JENOM pro tu překreslovanou). s tím caveatem, že pokud nějaké z těch dat nepůjde načíst, chci přesměrovat.
– Pokud budu načítat data v action nebo dříve → budu načítat a
potencionálně přesměrovávat kvůli komponentám, které vykreslovat ani
nebudu (v případě signálů, AJAX atd)
– Pokud budu načítat data až po provedení signálů (to jest nejdříve
v Presenter::beforeRender
), abych se vyhnul tomuto problému, tak
v render metodách už bych neměl přesměrovávat.
Proto se ptám, jak tohle řešít.
- Kamil Valenta
- Člen | 820
Asi bych zkusil něco ve smyslu:
public function actionDefault() {
if ($this->isAjax()) {
$s = $this->getSignal();
$signal = (is_array($s) ? implode('-', $s) : $s);
if ($signal == 'handleKteryPrekreslujeSnippet1') {
$this->template->data1 = $this->model->getData1();
} else if ($signal == 'handleKteryPrekreslujeSnippet2') {
$this->template->data2 = $this->model->getData2();
}
} else {
$this->template->data1 = $this->model->getData1();
$this->template->data2 = $this->model->getData2();
}
}
ALE! Vznikne tím samozřejmě dost nežádoucí vazba komponenty na
konkrétní presenter. Pokud tu komponentu znovupoužiješ v jiném presenteru,
už to takto dělat nebude.
Samozřejmě se i nabízí otázka, proč ta komponenta není soběstačná,
proč nemá model v závislostech a data si nezjišťuje sama.
- pata.kusik111
- Člen | 78
Šaman napsal(a):
Obecně můžeš překreslovat blíže neurčitý počet komponent. A všech komponent v těch komponentách.
Nestačilo by, aby si každá komponenta zkontrolovala svá data v konstruktoru a na úrovni presenteru odchytit případné výjimky a na ně reagovat?
Samozřejmě, že by to stačilo. Ale na úrovni presenteru kde přesně? Pokud tu komponentu nevytvořím dopředu, ale až když na ní narazím při rendrování v šabloně, tak to je pozdě na vyhazování vyjímek. A pokud si jí vytvořím dostatečně dopředu na to, abych mohl reagovat na vyjímky, tak budu vytvářet při signálech/AJAX requestech všechny komponenty, i ty, kterých se ten signál netýká. To je právě ten problém, na který hledám odpověď.
- pata.kusik111
- Člen | 78
Kamilův kód mi dal nápad, co si o tom myslíte? Je to trošku konvoluted,
ale když nebudu mít žádné handle
v presenteru, jenom
v komponentách a callbacky, tak „půlka“ z toho zmizí.
public function actionDefault() {
if ($this->isAjax()) {
$signal = $this->getSignal();
$componentName = $this->getComponentNameFromSignal($signal);
if($componentName !== '') { //empty string means signal to this presenter
try{
$this->getComponent($componentName, throw: true)
} catch(\Throwable) {
// I cannot fulfill the request
}
} else {
try {
if ($signal == 'handleKteryPrekreslujeSnippet1') {
$this->getComponent('criticalComponentForSnippet1', throw: true)
} else if ($signal == 'handleKteryPrekreslujeSnippet2') {
$this->getComponent('criticalComponentForSnippet2', throw: true)
}
} catch(\Throwable) {
// I cannot fulfill the request
}
}
} else {
try {
$this->getComponent('criticalComponentForSnippet1', throw: true)
$this->getComponent('criticalComponentForSnippet1', throw: true)
$this->getComponent('criticalComponentOutsideOfAnySnippets', throw: true)
} catch(\Throwable) {
// I cannot fulfill the request
}
}
}
- Marek Bartoš
- Nette Blogger | 1274
A co je špatného na tom inicializovat komponenty v beforeRender()?
- pata.kusik111
- Člen | 78
Mabar napsal(a):
A co je špatného na tom inicializovat komponenty v beforeRender()?
public function beforeRender()
{
try {
$dataForComponent = $this->model->getData(); //this can throw an exception if data is not available
} catch (\Throwable) {
//In this case I want to redirect to another page as this page does not make sense if I cannot render this component
//But isn't `beforeRender` too late for a redirect?
}
$this->addComponent($this->componentFactory($dataForComponent), 'componentName');
}
Editoval pata.kusik111 (16. 1. 2021 21:18)
- Marek Bartoš
- Nette Blogger | 1274
Dokud se neodeslaly http hlavičky, tak není pozdě na redirect. V beforeRender tedy můžeš bez obav přesměrovat.
Btw, klidně přesuň try-catch do createComponent*
metod,
v beforeRender ti pro inicializaci komponenty stačí k ní jen přistoupit.
Třeba přes $this['nazevKomponenty-nazevPodkomponenty']
- Kamil Valenta
- Člen | 820
Mabar napsal(a):
A co je špatného na tom inicializovat komponenty v beforeRender()?
Jak to řeší situaci, že je na stránce 5 komponent a jen 1 se invaliduje, přičemž modely pro ty 4 nemají být požádány o data?
- Marek Bartoš
- Nette Blogger | 1274
Pokud jde request na komponentu, tak ti ji Nette vytvoří samo po skončení action metody a před zpracováním signálů. V beforeRender() tak stačí podmínečně vytvářet jen komponenty, které se při renderu nutně nemusí vytvořit, nemusíš vůbec řešit signály.
- Kamil Valenta
- Člen | 820
Ale jak je budeš podmínečně vytvářet?
Překreslení může vyvolat třeba jen ajaxový odkaz na handle, nebo ajaxové
odeslání formuláře.
Komponenty mají být dle zadání krmeny daty zvenčí…
V beforeRenderu už budou požadované komponenty instancované, tam je přeci později řešit data pro jejich konstruktory…
Editoval Kamil Valenta (16. 1. 2021 22:30)
- pata.kusik111
- Člen | 78
@Felix cca něco takového?
/**
* @property-read AthleteDetailTemplate $template
*/
final class AthleteDetailPresenter extends BasePresenter
{
private const FROM_INDIVIDUAL_PLAN = 'Individual plan';
/**
* @persistent
*/
public string $id;
private Athlete $athlete;
private AthleteDomain $athleteDomain;
private ClubDomain $clubDomain;
private CalendarDomain $calendarDomain;
public function __construct(
ClubDomain $clubDomain,
AthleteDomain $athleteDomain,
CalendarDomain $calendarDomain
) {
parent::__construct();
$this->athleteDomain = $athleteDomain;
$this->clubDomain = $clubDomain;
$this->calendarDomain = $calendarDomain;
}
public function startup(): void
{
parent::startup();
$athlete = $this->athleteDomain->athlete($this->club, $this->id);
if ($athlete instanceof Athlete) {
$this->athlete = $athlete;
$this->template->item = $athlete;
} else {
$this->flashMessage('Athlete not found', 'warning');
$this->redirect('Athlete:'); //Přesměrování na rozcestník z detailu
}
try {
$control = $this->createComponentSubmenu(); //Tohle je Kritická komponenta pro každou podstránku - musí ji jít vykreslit aby stránka měla smysl.
if($control === null) {
throw new InvalidStateException('Could not create submenu component.'); //InvalidState, protože je stejná jako z `addComponent`
}
$this->addComponent($control, 'submenu');
} catch (InvalidStateException $exception) {
$this->logger->error('Cannot create critical component.', [
'exception' => $exception,
]);
$this->flashMessage('Athlete section not available at the moment', 'danger');
$this->redirect('Dashboard:'); //Nejsem schopen získat ani seznam atletů pro rozcestník, tak na přesměrováním na něco ještě základnějšího
}
}
/**
* @throws \Nette\Application\AbortException
*/
public function actionDevelopment(): void
{
//Tohle bych chtěl aby se dělo jenom pro rederování,
//ale když bude subrequest (na presenter nebo na nějakou jinou komponentu),
//který se této komponenty netýká, tak aby se to nedělo
try {
$control = $this->createComponentCalendar(); //Calendar je kritický jenom pro `Develpment`
if($control === null) {
throw new InvalidStateException('Could not create calendar component.');
}
$this->addComponent($control, 'calendar');
} catch (InvalidStateException $exception) {
$this->logger->error('Cannot create critical component.', [
'exception' => $exception,
]);
$this->flashMessage('Athlete development section not available at the moment', 'danger');
$this->redirect('overview'); //Redirect na rozcestník pro detail.
}
}
public function createComponentCalendar(): ?Component
{
try {
//Tenhle call je "drahý" a navíc může vyhazovat exception!!!
$clubPlanEvents = $this->calendarDomain->athleteClubPlanEvents($this->club, $this->id);
$calendarControl = new TrainingPlanCalendarControl(
[
...array_map(static fn ($name, $events): array => [
'id' => $name,
'events' => $events,
], array_keys($clubPlanEvents), $clubPlanEvents),
self::FROM_INDIVIDUAL_PLAN => [
'id' => self::FROM_INDIVIDUAL_PLAN,
//Tenhle call je "drahý" a navíc může vyhazovat exception!!!
'events' => $this->calendarDomain->athleteEventsFromIndividualPlan($this->club, $this->id),
],
],
self::FROM_INDIVIDUAL_PLAN
);
$calendarControl->onCalendarUpdate[] = function (array $events): void {
$this->athleteDomain->changeIndividualTraining($this->club, $this->id, $events);
};
return $calendarControl;
} catch (RuntimeException $exception) {
return null;
}
}
public function createComponentSubmenu(): ?Component
{
try {
$submenu = new AthleteSubmenu($this->athleteDomain->athleteNames($this->club), $this->id);
$submenu->onPick[] = function (?string $athleteId): void {
if ($athleteId === null) {
$this->redirect('Athlete:default');
}
$this->redirect('AthleteDetail:overview', [
'id' => $athleteId,
]);
};
$submenu->onAdd[] = function (string $firstName, string $lastName): void {
$athleteId = $this->clubDomain->createAthlete($this->club, $firstName, $lastName);
$this->flashMessage('Athlete created!', 'success');
$this->redirect('AthleteDetail:overview', [
'id' => $athleteId,
]);
};
return $submenu;
} catch (RuntimeException $exception) {
return null;
}
}
}