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.
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()
inforward()
preusmerierror()
zaključi presenter zaradi napakesendJson($data)
presenter zaključi in pošlje podatke v formatu JSONsendTemplate()
presenter zaključi in takoj izriše predlogosendResponse($response)
presenter zaključi in pošlje lastni odgovorterminate()
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:
- Nette\Application\Responses\CallbackResponse – pošlje povratni klic
- Nette\Application\Responses\FileResponse – pošlje datoteko
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – pošlje JSON
- Nette\Application\Responses\RedirectResponse – preusmeritev
- Nette\Application\Responses\TextResponse – pošlje besedilo
- Nette\Application\Responses\VoidResponse – prazen odgovor
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.