Presenterji

Spoznali bomo, kako se v Nette pišejo presenterji in predloge. Po branju boste vedeli:

  • kako deluje presenter
  • kaj so persistentni parametri
  • kako se rišejo predloge

Že vemo, da je presenter razred, ki predstavlja neko konkretno stran spletne aplikacije, npr. domačo stran; izdelek v spletni trgovini; prijavni obrazec; sitemap vir itd. Aplikacija lahko ima od enega do tisoč presenterjev. V drugih ogrodjih jim rečejo tudi kontrolerji.

Običajno se pod pojmom presenter misli na potomca razreda Nette\Application\UI\Presenter, ki je primeren za generiranje spletnih vmesnikov in kateremu se bomo posvetili v preostanku tega poglavja. V splošnem smislu je presenter katerikoli objekt, ki implementira vmesnik Nette\Application\IPresenter.

Življenjski cikel presenterja

Naloga presenterja je obdelati zahtevek in vrniti odgovor (kar je lahko HTML stran, slika, preusmeritev itd.).

Torej na začetku mu je predan zahtevek. To ni neposredno HTTP zahtevek, ampak objekt Nette\Application\Request, v katerega je bil HTTP zahtevek preoblikovan s pomočjo usmerjevalnika. S tem objektom običajno ne pridemo v stik, saj presenter obdelavo zahtevka pametno delegira v druge metode, ki si jih bomo zdaj pokazali.

Življenjski cikel presenterja

Slika predstavlja seznam metod, ki se postopoma od zgoraj navzdol kličejo, če obstajajo. Nobena od njih ni nujno, da obstaja, lahko imamo popolnoma prazen presenter brez ene same metode in na njem zgradimo preprosto statično spletno stran.

__construct()

Konstruktor ne spada povsem v življenjski cikel presenterja, ker se kliče v trenutku ustvarjanja objekta. Vendar ga navajamo zaradi pomembnosti. Konstruktor (skupaj z metodo inject) služi za posredovanje odvisnosti.

Presenter ne bi smel opravljati poslovne logike aplikacije, pisati in brati iz podatkovne baze, izvajati izračunov itd. Za to so razredi iz plasti, ki jo označujemo kot model. Na primer, razred ArticleRepository lahko skrbi za nalaganje in shranjevanje člankov. Da bi lahko presenter z njim delal, si ga pusti posredovati s pomočjo dependency injection:

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

startup()

Takoj po prejemu zahtevka se pokliče metoda startup(). Lahko jo uporabite za inicializacijo lastnosti, preverjanje uporabniških dovoljenj itd. Zahtevano je, da metoda vedno pokliče prednika parent::startup().

action<Action>(args...)

Podobno metodi render<View>(). Medtem ko je render<View>() namenjena pripravi podatkov za konkretno predlogo, ki se nato izriše, se v action<Action>() obdeluje zahtevek brez povezave z izrisovanjem predloge. Na primer, obdelajo se podatki, prijavi ali odjavi uporabnik, in tako naprej, nato pa preusmeri drugam.

Pomembno je, da se action<Action>() kliče prej kot render<View>(), tako da lahko v njej morebiti spremenimo nadaljnji potek dogodkov, tj. spremenimo predlogo, ki se bo risala, in tudi metodo render<View>(), ki se bo klicala. In to s pomočjo setView('jineView').

Metodi se posredujejo parametri iz zahtevka. Možno in priporočljivo je navesti tipe parametrov, npr. actionShow(int $id, ?string $slug = null) – če bo parameter id manjkal ali če ne bo integer, bo presenter vrnil napako 404 in zaključil delovanje.

handle<Signal>(args...)

Metoda obdeluje t.i. signale, s katerimi se bomo seznanili v poglavju, posvečenem komponentam. Namenjena je namreč predvsem komponentam in obdelavi AJAX zahtevkov.

Metodi se posredujejo parametri iz zahtevka, kot v primeru action<Action>(), vključno s tipsko kontrolo.

beforeRender()

Metoda beforeRender, kot že ime pove, se kliče pred vsako metodo render<View>(). Uporablja se za skupno konfiguracijo predloge, posredovanje spremenljivk za postavitev in podobno.

render<View>(args...)

Mesto, kjer pripravljamo predlogo za nadaljnje izrisovanje, ji posredujemo podatke itd.

Metodi se posredujejo parametri iz zahtevka, kot v primeru action<Action>(), vključno s tipsko kontrolo.

public function renderShow(int $id): void
{
	// pridobimo podatke iz modela in jih posredujemo predlogi
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Metoda afterRender, kot ime spet pove, se kliče za vsako metodo render<View>(). Uporablja se bolj izjemoma.

shutdown()

Kliče se na koncu življenjskega cikla presenterja.

Dober nasvet, preden gremo naprej. Presenter, kot je vidno, lahko obravnava več akcij/view, torej ima več metod render<View>(). Vendar priporočamo načrtovanje presenterjev z eno ali čim manj akcijami.

Pošiljanje odgovora

Odgovor presenterja je praviloma izris predloge s HTML stranjo, lahko pa je tudi pošiljanje datoteke, JSON ali pa preusmeritev na drugo stran.

Kadarkoli med življenjskim ciklom lahko z eno od naslednjih metod pošljemo odgovor in hkrati zaključimo presenter:

  • redirect(), redirectPermanent(), redirectUrl() in forward() preusmeri
  • error() zaključi presenter zaradi napake
  • sendJson($data) presenter zaključi in pošlje podatke v formatu JSON
  • sendTemplate() presenter zaključi in takoj izriše predlogo
  • sendResponse($response) presenter zaključi in pošlje lastni odgovor
  • terminate() presenter zaključi brez odgovora

Če nobene od teh metod ne pokličete, bo presenter samodejno pristopil k izrisu predloge. Zakaj? Ker v 99 % primerov želimo izrisati predlogo, zato presenter to obnašanje jemlje kot privzeto in nam želi olajšati delo.

Ustvarjanje povezav

Presenter razpolaga z metodo link(), s pomočjo katere lahko ustvarjamo URL povezave na druge presenterje. Prvi parameter je ciljni presenter & akcija, sledijo posredovani argumenti, ki so lahko navedeni kot polje:

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

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

V predlogi se ustvarjajo povezave na druge presenterje & akcije na ta način:

<a n:href="Product:show $id">podrobnosti izdelka</a>

Preprosto namesto realnega URL-ja napišete znani par Presenter:action in navedete morebitne parametre. Trik je v n:href, ki pravi, da ta atribut obdela Latte in generira realni URL. V Nette tako sploh ni treba razmišljati o URL-jih, samo o presenterjih in akcijah.

Več informacij najdete v poglavju Ustvarjanje URL povezav.

Preusmerjanje

Za prehod na drug presenter služita metodi redirect() in forward(), ki imata zelo podobno sintakso kot metoda link().

Metoda forward() preide na nov presenter takoj brez HTTP preusmeritve:

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

Primer t.i. začasne preusmeritve s HTTP kodo 302 (ali 303, če je metoda trenutnega zahtevka POST):

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

Trajno preusmeritev s HTTP kodo 301 dosežete takole:

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

Na drug URL izven aplikacije lahko preusmerite z metodo redirectUrl(). Kot drugi parameter lahko navedete HTTP kodo, privzeta je 302 (ali 303, če je metoda trenutnega zahtevka POST):

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

Preusmeritev takoj zaključi delovanje presenterja s sprožitvijo t.i. tihe zaključne izjeme Nette\Application\AbortException.

Pred preusmeritvijo lahko pošljete flash message, torej sporočila, ki bodo po preusmeritvi prikazana v predlogi.

Flash sporočila

Gre za sporočila, ki običajno obveščajo o rezultatu neke operacije. Pomembna značilnost flash sporočil je, da so v predlogi na voljo tudi po preusmeritvi. Tudi po prikazu ostanejo živa še nadaljnjih 30 sekund – na primer za primer, če bi zaradi napačnega prenosa uporabnik osvežil stran – sporočilo mu torej ne izgine takoj.

Dovolj je poklicati metodo flashMessage() in za posredovanje v predlogo poskrbi presenter. Prvi parameter je besedilo sporočila in neobvezni drugi parameter je njegov tip (error, warning, info ipd.). Metoda flashMessage() vrne instanco flash sporočila, kateremu je mogoče dodajati dodatne informacije.

$this->flashMessage('Element je bil izbrisan.');
$this->redirect(/* ... */); // in preusmerimo

Predlogi so ta sporočila na voljo v spremenljivki $flashes kot objekti stdClass, ki vsebujejo lastnosti message (besedilo sporočila), type (tip sporočila) in lahko vsebujejo že omenjene uporabniške informacije. Izrišemo jih na primer takole:

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

Napaka 404 in podobno

Če zahteve ni mogoče izpolniti, na primer zato, ker članek, ki ga želimo prikazati, ne obstaja v podatkovni bazi, sprožimo napako 404 z metodo error(?string $message = null, int $httpCode = 404).

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

HTTP kodo napake lahko predamo kot drugi parameter, privzeta je 404. Metoda deluje tako, da sproži izjemo Nette\Application\BadRequestException, nato pa Application preda nadzor error-presenterju. Kar je presenter, katerega naloga je prikazati stran, ki obvešča o nastali napaki. Nastavitev error-preseterja se izvaja v konfiguraciji application.

Pošiljanje JSON

Primer action-metode, ki pošlje podatke v formatu JSON in zaključi presenter:

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

Parametri zahtevka

Presenter in tudi vsaka komponenta pridobiva iz HTTP zahtevka svoje parametre. Njihovo vrednost ugotovite z metodo getParameter($name) ali getParameters(). Vrednosti so nizi ali polja nizov, gre v bistvu za surove podatke, pridobljene neposredno iz URL-ja.

Za večje udobje priporočamo, da parametre zpřístupnite prek lastnosti. Dovolj je, da jih označite z atributom #[Parameter]:

use Nette\Application\Attributes\Parameter;  // ta vrstica je pomembna

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // mora biti public
}

Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. string) in Nette glede na to vrednost samodejno preoblikuje. Vrednosti parametrov lahko tudi validirate.

Pri ustvarjanju povezave lahko parametrom vrednost neposredno nastavite:

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

Persistentni parametri

Persistentni parametri služijo za ohranjanje stanja med različnimi zahtevki. Njihova vrednost ostane enaka tudi po kliku na povezavo. Za razliko od podatkov v seji se prenašajo v URL-ju. In to popolnoma samodejno, ni jih torej treba eksplicitno navajati v link() ali n:href.

Primer uporabe? Imate večjezično aplikacijo. Trenutni jezik je parameter, ki mora biti nenehno del URL-ja. Vendar bi bilo izjemno utrujajoče ga v vsaki povezavi navajati. Zato ga naredite za persistentni parameter lang in se bo prenašal sam. Odlično!

Ustvarjanje persistentnega parametra je v Nette izjemno enostavno. Dovolj je ustvariti javno lastnost in jo označiti z atributom: (prej se je uporabljalo /** @persistent */)

use Nette\Application\Attributes\Persistent;  // ta vrstica je pomembna

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

Če bo $this->lang imel vrednost na primer 'en', bodo tudi povezave, ustvarjene s pomočjo link() ali n:href, vsebovale parameter lang=en. In po kliku na povezavo bo spet $this->lang = 'en'.

Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. string) in lahko navedete tudi privzeto vrednost. Vrednosti parametrov lahko validirate.

Persistentni parametri se standardno prenašajo med vsemi akcijami danega presenterja. Da bi se prenašali tudi med več presenterji, jih je treba definirati bodisi:

  • v skupnem predniku, od katerega presenterji dedujejo
  • v traiti, ki jo presenterji uporabijo:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

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

Pri ustvarjanju povezave lahko persistentnemu parametru spremenite vrednost:

<a n:href="Product:show $id, lang: sl">podrobnosti v slovenščini</a>

Nebo jej lze vyresetovat, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu:

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

Interaktivne komponente

Presenterji imajo vgrajen komponentni sistem. Komponente so samostojne ponovno uporabne celote, ki jih vstavljamo v presenterje. Lahko so obrazci, podatkovne mreže, meniji, pravzaprav karkoli, kar ima smisel uporabljati večkrat.

Kako se komponente vstavljajo v presenter in nato uporabljajo? To boste izvedeli v poglavju Komponente. Celo ugotovili boste, kaj imajo skupnega s Hollywoodom.

In kje lahko dobim komponente? Na strani Componette najdete odprtokodne komponente in tudi vrsto drugih dodatkov za Nette, ki so jih sem postavili prostovoljci iz skupnosti okoli ogrodja.

Gremo v globino

S tem, kar smo si doslej v tem poglavju pokazali, si boste najverjetneje popolnoma zadostovali. Naslednje vrstice so namenjene tistim, ki se zanimajo za presenterje v globino in želijo vedeti popolnoma vse.

Validacija parametrov

Vrednosti parametrov zahtevka in persistentnih parametrov, prejetih iz URL-ja, zapisuje v lastnosti metoda loadState(). Ta tudi kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí.

Nikoli slepo ne verjemite parametrom, saj jih lahko uporabnik enostavno prepiše v URL-ju. Tako na primer preverimo, ali je jezik $this->lang med podprtimi. Primerna pot je prepisati omenjeno metodo loadState():

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

	public function loadState(array $params): void
	{
		parent::loadState($params); // tukaj se nastavi $this->lang
		// sledi lastno preverjanje vrednosti:
		if (!in_array($this->lang, ['en', 'sl'])) { // 'cs' spremenjeno v 'sl'
			$this->error();
		}
	}
}

Shranjevanje in obnovitev zahtevka

Zahtevek, ki ga obravnava presenter, je objekt Nette\Application\Request in ga vrača metoda presenterja getRequest().

Trenutni zahtevek lahko shranimo v sejo ali pa ga iz nje obnovimo in pustimo, da ga presenter ponovno izvede. To je koristno na primer v situaciji, ko uporabnik izpolnjuje obrazec in mu poteče prijava. Da ne bi izgubil podatkov, pred preusmeritvijo na prijavno stran trenutni zahtevek shranimo v sejo s pomočjo $reqId = $this->storeRequest(), ki vrne njegov identifikator v obliki kratkega niza in ga predamo kot parameter prijavnemu presenterju.

Po prijavi pokličemo metodo $this->restoreRequest($reqId), ki zahtevek prevzame iz seje in preusmeri nanj. Metoda pri tem preveri, da je zahtevek ustvaril isti uporabnik, kot se je zdaj prijavil. Če bi se prijavil drug uporabnik ali bi bil ključ neveljaven, ne naredi nič in program nadaljuje naprej.

Poglejte si navodilo Kako se vrniti na prejšnjo stran.

Kanonizacija

Presenterji imajo eno resnično odlično lastnost, ki prispeva k boljšemu SEO (optimizaciji najdljivosti na internetu). Samodejno preprečujejo obstoj podvojene vsebine na različnih URL-jih. Če do določenega cilja vodi več URL naslovov, npr. /index in /index?page=1, ogrodje določi enega od njih za primarnega (kanoničnega) in ostale nanj preusmeri s pomočjo HTTP kode 301. Zahvaljujoč temu vam iskalniki strani ne indeksirajo dvakrat in ne razpršijo njihovega page ranka.

Temu procesu rečemo kanonizacija. Kanonični URL je tisti, ki ga generira usmerjevalnik, praviloma torej prva ustrezna pot v zbirki.

Kanonizacija je privzeto vklopljena in jo lahko izklopite prek $this->autoCanonicalize = false.

Do preusmeritve ne pride pri AJAX ali POST zahtevku, ker bi prišlo do izgube podatkov ali pa to ne bi imelo dodane vrednosti z vidika SEO.

Kanonizacijo lahko sprožite tudi ročno s pomočjo metode canonicalize(), kateri se podobno kot metodi link() predajo presenter, akcija in parametri. Izdelala bo povezavo in jo primerjala s trenutnim URL naslovom. Če se razlikujeta, preusmeri na generirano povezavo.

public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// preusmeri, če se $slug razlikuje od $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Dogodki

Poleg metod startup(), beforeRender() in shutdown(), ki se kličejo kot del življenjskega cikla presenterja, lahko definiramo še druge funkcije, ki naj se samodejno pokličejo. Presenter definira t.i. dogodke, katerih obdelovalce dodate v polja $onStartup, $onRender in $onShutdown.

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

Obdelovalci v polju $onStartup se kličejo tik pred metodo startup(), nato $onRender med beforeRender() in render<View>() in na koncu $onShutdown tik pred shutdown().

Odgovori

Odgovor, ki ga vrača presenter, je objekt, ki implementira vmesnik Nette\Application\Response. Na voljo je vrsta pripravljenih odgovorov:

Odgovori se pošiljajo z metodo sendResponse():

use Nette\Application\Responses;

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

// Pošlje datoteko
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Odgovor bo povratni klic
$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));

Omejitev dostopa s pomočjo #[Requires]

Atribut #[Requires] ponuja napredne možnosti za omejevanje dostopa do presenterjev in njihovih metod. Lahko ga uporabite za specifikacijo HTTP metod, zahtevanje AJAX zahtevka, omejitev na isti izvor (same origin), in dostop samo prek posredovanja (forwarding). Atribut lahko uporabite tako za razrede presenterjev kot za posamezne metode action<Action>(), render<View>(), handle<Signal>() in createComponent<Name>().

Lahko določite te omejitve:

  • na HTTP metode: #[Requires(methods: ['GET', 'POST'])]
  • zahtevanje AJAX zahtevka: #[Requires(ajax: true)]
  • dostop samo iz istega izvora: #[Requires(sameOrigin: true)]
  • dostop samo prek posredovanja: #[Requires(forward: true)]
  • omejitev na konkretne akcije: #[Requires(actions: 'default')]

Podrobnosti najdete v navodilu Kako uporabljati atribut Requires.

Preverjanje HTTP metode

Presenterji v Nette samodejno preverjajo HTTP metodo vsakega dohodnega zahtevka. Razlog za to preverjanje je predvsem varnost. Standardno so dovoljene metode GET, POST, HEAD, PUT, DELETE, PATCH.

Če želite dovoliti dodatno na primer metodo OPTIONS, uporabite za to atribut #[Requires] (od Nette Application v3.2):

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

V različici 3.1 se preverjanje izvaja v checkHttpMethod(), ki ugotavlja, ali je metoda, specificirana v zahtevku, vsebovana v polju $presenter->allowedMethods. Dodajanje metode naredite takole:

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

Pomembno je poudariti, da če dovolite metodo OPTIONS, jo morate nato tudi ustrezno obravnavati znotraj svojega presenterja. Metoda se pogosto uporablja kot t.i. preflight request, ki ga brskalnik samodejno pošlje pred dejanskim zahtevkom, ko je treba ugotoviti, ali je zahtevek dovoljen z vidika CORS (Cross-Origin Resource Sharing) politike. Če metodo dovolite, vendar ne implementirate pravilnega odgovora, lahko to vodi do neskladij in potencialnih varnostnih težav.

Nadaljnje branje

različica: 4.0