Presentere

Vom face cunoștință cu modul în care se scriu presenterele și șabloanele în Nette. După citire, veți ști:

  • cum funcționează un presenter
  • ce sunt parametrii persistenți
  • cum se redau șabloanele

Știm deja că presenterul este o clasă care reprezintă o anumită pagină specifică a aplicației web, de ex. pagina de start; un produs într-un magazin online; formularul de conectare; feed-ul sitemap etc. Aplicația poate avea de la unul la mii de presentere. În alte framework-uri li se mai spune și controllere.

De obicei, prin termenul presenter se înțelege un descendent al clasei Nette\Application\UI\Presenter, care este potrivit pentru generarea interfețelor web și căruia ne vom dedica în restul acestui capitol. În sens general, un presenter este orice obiect care implementează interfața Nette\Application\IPresenter.

Ciclul de viață al presenterului

Sarcina presenterului este de a gestiona cererea și de a returna un răspuns (care poate fi o pagină HTML, o imagine, o redirecționare etc.).

Deci, la început i se transmite cererea. Nu este direct o cerere HTTP, ci obiectul Nette\Application\Request, în care a fost transformată cererea HTTP cu ajutorul routerului. Cu acest obiect, de obicei, nu intrăm în contact, deoarece presenterul deleagă inteligent procesarea cererii către alte metode, pe care le vom prezenta acum.

Ciclul de viață al presenterului

Imaginea reprezintă lista metodelor care sunt apelate succesiv de sus în jos, dacă există. Niciuna dintre ele nu trebuie să existe, putem avea un presenter complet gol, fără nicio metodă, și să construim pe el un site web static simplu.

__construct()

Constructorul nu face parte în totalitate din ciclul de viață al presenterului, deoarece este apelat în momentul creării obiectului. Dar îl menționăm datorită importanței sale. Constructorul (împreună cu metoda inject) servește la transmiterea dependențelor.

Presenterul nu ar trebui să se ocupe de logica de business a aplicației, să scrie și să citească din baza de date, să efectueze calcule etc. Pentru asta există clase din stratul pe care îl numim model. De exemplu, clasa ArticleRepository poate avea responsabilitatea de a încărca și salva articole. Pentru ca presenterul să poată lucra cu ea, o primește transmisă prin dependency injection:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

Imediat după primirea cererii, se apelează metoda startup(). O puteți utiliza pentru inițializarea proprietăților, verificarea permisiunilor utilizatorului etc. Este necesar ca metoda să apeleze întotdeauna părintele parent::startup().

action<Action>(args...)

Similară cu metoda render<View>(). În timp ce render<View>() este destinată pregătirii datelor pentru un șablon specific care urmează să fie redat, în action<Action>() se procesează cererea fără legătură cu redarea șablonului. De exemplu, se procesează date, se conectează sau deconectează utilizatorul, și așa mai departe, și apoi se redirecționează în altă parte.

Important este că action<Action>() se apelează înainte de render<View>(), deci în ea putem eventual schimba cursul ulterior al evenimentelor, adică să schimbăm șablonul care va fi redat, și, de asemenea, metoda render<View>() care va fi apelată. Și asta folosind setView('jineView').

Metodei i se transmit parametri din cerere. Este posibil și recomandat să se specifice tipurile parametrilor, de ex. actionShow(int $id, ?string $slug = null) – dacă parametrul id lipsește sau dacă nu este un integer, presenterul va returna eroarea 404 și va încheia activitatea.

handle<Signal>(args...)

Metoda procesează așa-numitele semnale, cu care ne vom familiariza în capitolul dedicat componentelor. Este destinată în special componentelor și procesării cererilor AJAX.

Metodei i se transmit parametri din cerere, ca în cazul action<Action>(), inclusiv verificarea tipului.

beforeRender()

Metoda beforeRender, așa cum sugerează și numele, se apelează înainte de fiecare metodă render<View>(). Se utilizează pentru configurarea comună a șablonului, transmiterea variabilelor pentru layout și altele asemenea.

render<View>(args...)

Locul unde pregătim șablonul pentru redarea ulterioară, îi transmitem date etc.

Metodei i se transmit parametri din cerere, ca în cazul action<Action>(), inclusiv verificarea tipului.

public function renderShow(int $id): void
{
	// obținem datele din model și le transmitem șablonului
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Metoda afterRender, așa cum sugerează din nou numele, se apelează după fiecare metodă render<View>(). Se utilizează mai degrabă excepțional.

shutdown()

Se apelează la sfârșitul ciclului de viață al presenterului.

Un sfat bun, înainte de a merge mai departe. Presenterul, după cum se vede, poate gestiona mai multe acțiuni/view-uri, adică poate avea mai multe metode render<View>(). Dar recomandăm proiectarea presenterelor cu una sau cât mai puține acțiuni.

Trimiterea răspunsului

Răspunsul presenterului este, de regulă, redarea unui șablon cu o pagină HTML, dar poate fi și trimiterea unui fișier, JSON sau chiar o redirecționare către o altă pagină.

Oricând în timpul ciclului de viață putem trimite un răspuns folosind una dintre următoarele metode și, în același timp, să încheiem presenterul:

Dacă nu apelați niciuna dintre aceste metode, presenterul va trece automat la redarea șablonului. De ce? Deoarece în 99% din cazuri dorim să redăm un șablon, prin urmare presenterul consideră acest comportament ca fiind implicit și vrea să ne ușureze munca.

Crearea linkurilor

Presenterul dispune de metoda link(), cu ajutorul căreia se pot crea linkuri URL către alți presenteri. Primul parametru este presenterul & acțiunea țintă, urmate de argumentele transmise, care pot fi specificate ca array:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'cs']);

În șablon se creează linkuri către alți presenteri & acțiuni în acest mod:

<a n:href="Product:show $id">detaliu produs</a>

Pur și simplu, în loc de URL-ul real, scrieți perechea cunoscută Presenter:action și specificați eventualii parametri. Trucul este în n:href, care spune că acest atribut va fi procesat de Latte și va genera URL-ul real. În Nette, nu trebuie să vă gândiți deloc la URL-uri, ci doar la presentere și acțiuni.

Mai multe informații găsiți în capitolul Crearea linkurilor URL.

Redirecționare

Pentru a trece la un alt presenter se utilizează metodele redirect() și forward(), care au o sintaxă foarte similară cu metoda link().

Metoda forward() trece imediat la noul presenter fără redirecționare HTTP:

$this->forward('Product:show');

Exemplu de așa-numită redirecționare temporară cu codul HTTP 302 (sau 303, dacă metoda cererii curente este POST):

$this->redirect('Product:show', $id);

Redirecționarea permanentă cu codul HTTP 301 se realizează astfel:

$this->redirectPermanent('Product:show', $id);

Către un alt URL în afara aplicației se poate redirecționa cu metoda redirectUrl(). Ca al doilea parametru se poate specifica codul HTTP, implicit este 302 (sau 303, dacă metoda cererii curente este POST):

$this->redirectUrl('https://nette.org');

Redirecționarea încheie imediat activitatea presenterului prin aruncarea așa-numitei excepții de terminare silențioasă Nette\Application\AbortException.

Înainte de redirecționare se poate trimite un mesaj flash, adică mesaje care vor fi afișate în șablon după redirecționare.

Mesaje flash

Acestea sunt mesaje care informează de obicei despre rezultatul unei operațiuni. O caracteristică importantă a mesajelor flash este că sunt disponibile în șablon chiar și după redirecționare. Chiar și după afișare, rămân active încă 30 de secunde – de exemplu, în cazul în care utilizatorul ar reîncărca pagina din cauza unei erori de transmisie – mesajul nu dispare imediat.

Este suficient să apelați metoda flashMessage() și presenterul se va ocupa de transmiterea către șablon. Primul parametru este textul mesajului și al doilea parametru opțional este tipul său (error, warning, info etc.). Metoda flashMessage() returnează instanța mesajului flash, căreia i se pot adăuga informații suplimentare.

$this->flashMessage('Elementul a fost șters.');
$this->redirect(/* ... */); // și redirecționăm

În șablon, aceste mesaje sunt disponibile în variabila $flashes ca obiecte stdClass, care conțin proprietățile message (textul mesajului), type (tipul mesajului) și pot conține informațiile utilizatorului menționate anterior. Le redăm, de exemplu, astfel:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Eroare 404 etc.

Dacă cererea nu poate fi îndeplinită, de exemplu, pentru că articolul pe care dorim să îl afișăm nu există în baza de date, aruncăm eroarea 404 cu metoda error(?string $message = null, int $httpCode = 404).

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

Codul HTTP al erorii poate fi transmis ca al doilea parametru, implicit este 404. Metoda funcționează aruncând excepția Nette\Application\BadRequestException, după care Application predă controlul error-presenterului. Acesta este un presenter a cărui sarcină este să afișeze o pagină care informează despre eroarea apărută. Setarea error-presenterului se face în configurația application.

Trimiterea JSON

Exemplu de metodă-acțiune care trimite date în format JSON și încheie presenterul:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Parametrii cererii

Presenterul și, de asemenea, fiecare componentă obțin parametrii săi din cererea HTTP. Valoarea lor o aflați cu metoda getParameter($name) sau getParameters(). Valorile sunt șiruri de caractere sau array-uri de șiruri de caractere, sunt în esență date brute obținute direct din URL.

Pentru mai mult confort, recomandăm accesarea parametrilor prin proprietăți. Este suficient să le marcați cu atributul #[Parameter]:

use Nette\Application\Attributes\Parameter;  // această linie este importantă

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // trebuie să fie publică
}

Pentru proprietate, recomandăm să specificați și tipul de date (de ex. string), iar Nette va converti automat valoarea conform acestuia. Valorile parametrilor pot fi, de asemenea, validate.

La crearea unui link, valoarea parametrilor poate fi setată direct:

<a n:href="Home:default theme: dark">click</a>

Parametri persistenți

Parametrii persistenți sunt utilizați pentru a menține starea între diferite cereri. Valoarea lor rămâne aceeași chiar și după ce se face clic pe un link. Spre deosebire de datele din sesiune, acestea sunt transmise în URL. Și acest lucru se întâmplă complet automat, deci nu este necesar să le specificați explicit în link() sau n:href.

Exemplu de utilizare? Aveți o aplicație multilingvă. Limba curentă este un parametru care trebuie să fie constant parte a URL-ului. Dar ar fi incredibil de obositor să îl specificați în fiecare link. Așa că îl faceți un parametru persistent lang și se va transmite singur. Minunat!

Crearea unui parametru persistent în Nette este extrem de simplă. Este suficient să creați o proprietate publică și să o marcați cu un atribut: (anterior se folosea /** @persistent */)

use Nette\Application\Attributes\Persistent;  // această linie este importantă

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // trebuie să fie publică
}

Dacă $this->lang va avea valoarea, de exemplu, 'en', atunci și linkurile create folosind link() sau n:href vor conține parametrul lang=en. Și după ce se face clic pe link, va fi din nou $this->lang = 'en'.

Pentru proprietate, recomandăm să specificați și tipul de date (de ex. string) și puteți specifica și o valoare implicită. Valorile parametrilor pot fi validate.

Parametrii persistenți sunt transmiși standard între toate acțiunile presenterului respectiv. Pentru a se transmite și între mai mulți presenteri, este necesar să fie definiți fie:

  • într-un strămoș comun, de la care moștenesc presenterele
  • într-un trait, pe care presenterele îl utilizează:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

La crearea unui link, valoarea parametrului persistent poate fi modificată:

<a n:href="Product:show $id, lang: cs">detaliu în cehă</a>

Sau poate fi resetat, adică eliminat din URL. Atunci va lua valoarea sa implicită:

<a n:href="Product:show $id, lang: null">click aici</a>

Componente interactive

Presenterele au încorporat un sistem de componente. Componentele sunt unități separate, reutilizabile, pe care le inserăm în presentere. Acestea pot fi formulare, datagrid-uri, meniuri, de fapt, orice are sens să fie folosit în mod repetat.

Cum se inserează componentele în presenter și cum se utilizează ulterior? Acest lucru îl veți afla în capitolul Componente. Veți descoperi chiar și ce au în comun cu Hollywood-ul.

Și de unde pot obține componente? Pe pagina Componette găsiți componente open-source și, de asemenea, o serie de alte add-on-uri pentru Nette, pe care le-au plasat aici voluntari din comunitatea din jurul framework-ului.

Intrăm în profunzime

Cu ceea ce am arătat până acum în acest capitol, probabil vă veți descurca complet. Următoarele rânduri sunt destinate celor care sunt interesați de presentere în profunzime și doresc să știe absolut totul.

Validarea parametrilor

Valorile parametrilor cererii și ale parametrilor persistenți primite din URL sunt scrise în proprietăți de către metoda loadState(). Aceasta verifică, de asemenea, dacă tipul de date specificat la proprietate corespunde, altfel răspunde cu eroarea 404 și pagina nu se afișează.

Nu credeți niciodată orbește în parametri, deoarece pot fi ușor suprascriși de utilizator în URL. Astfel, de exemplu, verificăm dacă limba $this->lang se află printre cele suportate. O cale potrivită este să suprascriem metoda menționată loadState():

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // aici se setează $this->lang
		// urmează controlul propriu al valorii:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Salvarea și restaurarea cererii

Cererea pe care o gestionează presenterul este obiectul Nette\Application\Request și este returnată de metoda presenterului getRequest().

Cererea curentă poate fi salvată în sesiune sau, invers, restaurată din ea și lăsată presenterului să o execute din nou. Acest lucru este util, de exemplu, în situația în care utilizatorul completează un formular și îi expiră sesiunea de conectare. Pentru a nu pierde datele, înainte de redirecționarea către pagina de conectare, salvăm cererea curentă în sesiune folosind $reqId = $this->storeRequest(), care returnează identificatorul său sub forma unui șir scurt și îl transmitem ca parametru presenterului de conectare.

După conectare, apelăm metoda $this->restoreRequest($reqId), care preia cererea din sesiune și face forward către ea. Metoda verifică, în același timp, că cererea a fost creată de același utilizator care s-a conectat acum. Dacă s-ar conecta un alt utilizator sau cheia ar fi invalidă, nu face nimic și programul continuă.

Consultați ghidul Cum să reveniți la pagina anterioară.

Canonizare

Presenterele au o caracteristică cu adevărat grozavă, care contribuie la un SEO mai bun (optimizarea găsirii pe internet). Previn automat existența conținutului duplicat la URL-uri diferite. Dacă către o anumită țintă duc mai multe adrese URL, de ex. /index și /index?page=1, framework-ul o determină pe una dintre ele ca fiind primară (canonică) și le redirecționează pe celelalte către ea folosind codul HTTP 301. Datorită acestui fapt, motoarele de căutare nu vă indexează paginile de două ori și nu le diluează page rank-ul.

Acest proces se numește canonizare. URL-ul canonic este cel generat de router, de regulă deci prima rută corespunzătoare din colecție.

Canonizarea este activată implicit și poate fi dezactivată prin $this->autoCanonicalize = false.

Redirecționarea nu are loc la o cerere AJAX sau POST, deoarece s-ar pierde date sau nu ar avea valoare adăugată din punct de vedere SEO.

Canonizarea poate fi invocată și manual folosind metoda canonicalize(), căreia i se transmit, similar metodei link(), presenterul, acțiunea și parametrii. Creează un link și îl compară cu URL-ul curent. Dacă diferă, redirecționează către linkul generat.

public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// redirecționează, dacă $slug diferă de $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Evenimente

Pe lângă metodele startup(), beforeRender() și shutdown(), care sunt apelate ca parte a ciclului de viață al presenterului, pot fi definite și alte funcții care să fie apelate automat. Presenterul definește așa-numitele evenimente, ale căror handlere le adăugați în array-urile $onStartup, $onRender și $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Handlerele din array-ul $onStartup sunt apelate chiar înainte de metoda startup(), apoi $onRender între beforeRender() și render<View>() și în final $onShutdown chiar înainte de shutdown().

Răspunsuri

Răspunsul returnat de presenter este un obiect care implementează interfața Nette\Application\Response. Există o serie de răspunsuri pregătite disponibile:

Răspunsurile sunt trimise prin metoda sendResponse():

use Nette\Application\Responses;

// Text simplu
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Trimite fișier
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Răspunsul va fi un callback
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Hello</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Restricționarea accesului folosind #[Requires]

Atributul #[Requires] oferă opțiuni avansate pentru restricționarea accesului la presentere și metodele lor. Poate fi utilizat pentru specificarea metodelor HTTP, solicitarea unei cereri AJAX, restricționarea la aceeași origine (same origin) și accesul doar prin forward. Atributul poate fi aplicat atât claselor presenterelor, cât și metodelor individuale action<Action>(), render<View>(), handle<Signal>() și createComponent<Name>().

Puteți specifica aceste restricții:

  • pentru metode HTTP: #[Requires(methods: ['GET', 'POST'])]
  • solicitarea unei cereri AJAX: #[Requires(ajax: true)]
  • acces doar din aceeași origine: #[Requires(sameOrigin: true)]
  • acces doar prin forward: #[Requires(forward: true)]
  • restricționare la acțiuni specifice: #[Requires(actions: 'default')]

Detalii găsiți în ghidul Cum se utilizează atributul Requires.

Verificarea metodei HTTP

Presenterele din Nette verifică automat metoda HTTP a fiecărei cereri primite. Motivul acestei verificări este în principal securitatea. Standard, sunt permise metodele GET, POST, HEAD, PUT, DELETE, PATCH.

Dacă doriți să permiteți în plus, de exemplu, metoda OPTIONS, utilizați atributul #[Requires] (de la Nette Application v3.2):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

În versiunea 3.1, verificarea se face în checkHttpMethod(), care verifică dacă metoda specificată în cerere este inclusă în array-ul $presenter->allowedMethods. Adăugarea metodei se face astfel:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

Este important de subliniat că, dacă permiteți metoda OPTIONS, trebuie ulterior să o gestionați corespunzător în cadrul presenterului dvs. Metoda este adesea utilizată ca așa-numită cerere preflight, pe care browserul o trimite automat înainte de cererea reală, când este necesar să se afle dacă cererea este permisă din punctul de vedere al politicii CORS (Cross-Origin Resource Sharing). Dacă permiteți metoda, dar nu implementați un răspuns corect, acest lucru poate duce la inconsecvențe și potențiale probleme de securitate.

Lectură suplimentară

versiune: 4.0