Jak zjišťujete, jestli komponenta půjde vytvořit?

pata.kusik111
Člen | 78
+
0
-

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

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

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

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.

Šaman
Člen | 2525
+
+1
-

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?

pata.kusik111
Člen | 78
+
0
-

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

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
		}
    }
}
Mabar
Člen | 350
+
0
-

A co je špatného na tom inicializovat komponenty v beforeRender()?

pata.kusik111
Člen | 78
+
0
-

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. 21:18)

Mabar
Člen | 350
+
0
-

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

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?

Mabar
Člen | 350
+
-1
-

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

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. 22:30)

Felix
Nette Core | 1072
+
+1
-

@patakusik111 Hodil by jsi prosim konkretni priklad presenteru, komponenty a pripadu kdy se komponenta ma vykreslit a kdy ne?

pata.kusik111
Člen | 78
+
0
-

@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;
        }
    }
}