Interaktivne komponente
Komponente so samostojni ponovno uporabni objekti, ki jih vstavljamo v strani. Lahko so obrazci, podatkovne mreže, ankete, pravzaprav karkoli, kar ima smisel uporabljati večkrat. Pokazali si bomo:
- kako uporabljati komponente?
- kako jih pisati?
- kaj so signali?
Nette ima vgrajen komponentni sistem. Nekaj podobnega se lahko spomnijo veterani iz Delphi ali ASP.NET Web Forms, na nečem oddaljeno podobnem temeljita React ali Vue.js. Vendar pa je v svetu PHP ogrodij to edinstvena zadeva.
Pri tem komponente bistveno vplivajo na pristop k ustvarjanju aplikacij. Strani lahko namreč sestavljate iz vnaprej pripravljenih enot. Potrebujete v administraciji podatkovno mrežo? Najdete jo na Componette, repozitoriju odprtokodnih dodatkov (torej ne samo komponent) za Nette in jo preprosto vstavite v presenter.
V presenter lahko vključite poljubno število komponent. In v nekatere komponente lahko vstavljate druge komponente. Tako nastane komponentno drevo, katerega koren je presenter.
Tovarniške metode
Kako se komponente vstavljajo v presenter in nato uporabljajo? Običajno s pomočjo tovarniških metod.
Tovarna komponent predstavlja eleganten način, kako komponente ustvarjati šele takrat, ko so dejansko potrebne (lazy / on
demand). Celotna čarovnija temelji na implementaciji metode z imenom createComponent<Name>()
, kjer je
<Name>
ime ustvarjene komponente, in ki komponento ustvari ter vrne.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Zahvaljujoč temu, da so vse komponente ustvarjene v ločenih metodah, koda pridobi na preglednosti.
Imena komponent se vedno začnejo z malo začetnico, čeprav se v imenu metode pišejo z veliko.
Tovarn nikoli ne kličemo neposredno, pokličejo se same takrat, ko komponento prvič uporabimo. Zahvaljujoč temu je komponenta ustvarjena v pravem trenutku in samo v primeru, ko je dejansko potrebna. Če komponente ne uporabimo (na primer pri AJAX zahtevku, ko se prenaša samo del strani, ali pri predpomnjenju predloge), se sploh ne ustvari in prihranimo zmogljivost strežnika.
// dostopimo do komponente in če je bilo to prvič,
// se pokliče createComponentPoll(), ki jo ustvari
$poll = $this->getComponent('poll');
// alternativna sintaksa: $poll = $this['poll'];
V predlogi je mogoče izrisati komponento s pomočjo značke {control}. Zato ni potrebno ročno posredovati komponent v predlogo.
<h2>Glasujte</h2>
{control poll}
Hollywood style
Komponente običajno uporabljajo eno svežo tehniko, ki ji radi rečemo Hollywood style. Zagotovo poznate krilatico, ki jo tako pogosto slišijo udeleženci filmskih avdicij: “Ne kličite nas, mi bomo poklicali vas.” In prav za to gre.
V Nette namreč namesto tega, da bi se morali nenehno spraševati (“je bil obrazec poslan?”, “je bil veljaven?” ali “je uporabnik pritisnil ta gumb?”), poveste ogrodju “ko se to zgodi, pokliči to metodo” in nadaljnje delo prepustite njemu. Če programirate v JavaScriptu, ta slog programiranja dobro poznate. Pišete funkcije, ki se kličejo, ko nastopi določen dogodek. In jezik jim posreduje ustrezne parametre.
To popolnoma spremeni pogled na pisanje aplikacij. Več nalog kot lahko prepustite ogrodju, manj dela imate vi. In manj stvari lahko na primer pozabite.
Pišemo komponento
Pod pojmom komponenta običajno mislimo na potomca razreda Nette\Application\UI\Control. (Natančneje
bi bilo torej uporabljati izraz “controls”, vendar “kontrole” imajo v slovenščini popolnoma drugačen pomen in se je
bolj uveljavil izraz “komponente”.) Sam presenter Nette\Application\UI\Presenter je
mimogrede tudi potomec razreda Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Izrisovanje
Že vemo, da se za izris komponente uporablja značka {control componentName}
. Ta pravzaprav pokliče metodo
render()
komponente, v kateri poskrbimo za izris. Na voljo imamo, popolnoma enako kot v presenterju, Latte predlogo v spremenljivki $this->template
,
v katero posredujemo parametre. Za razliko od presenterja moramo navesti datoteko s predlogo in jo pustiti izrisati:
public function render(): void
{
// vstavimo v predlogo nekaj parametrov
$this->template->param = $value;
// in jo izrišemo
$this->template->render(__DIR__ . '/poll.latte');
}
Značka {control}
omogoča posredovanje parametrov v metodo render()
:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Včasih se lahko komponenta sestoji iz več delov, ki jih želimo izrisovati ločeno. Za vsakega od njih si ustvarimo lastno
metodo za izris, tukaj v primeru na primer renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
In v predlogi jo nato pokličemo s pomočjo:
{control poll:paginator}
Za boljše razumevanje je dobro vedeti, kako se ta značka prevede v PHP.
{control poll}
{control poll:paginator 123, 'hello'}
se prevede kot:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Metoda getComponent()
vrne komponento poll
in nad to komponento kliče metodo render()
,
oz. renderPaginator()
, če je drugačen način izrisovanja naveden v znački za dvopičjem.
Pozor, če se kjerkoli v parametrih pojavi =>
, bodo vsi parametri zapakirani v polje
in posredovani kot prvi argument:
{control poll, id: 123, message: 'hello'}
se prevede kot:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Izris podkomponente:
{control cartControl-someForm}
se prevede kot:
$control->getComponent("cartControl-someForm")->render();
Komponente, enako kot presenterji, samodejno posredujejo v predloge nekaj uporabnih spremenljivk:
$basePath
je absolutna URL pot do korenskega direktorija (npr./eshop
)$baseUrl
je absolutni URL do korenskega direktorija (npr.http://localhost/eshop
)$user
je objekt ki predstavlja uporabnika$presenter
je trenutni presenter$control
je trenutna komponenta$flashes
polje sporočil poslanih s funkcijoflashMessage()
Signal
Že vemo, da navigacija v Nette aplikaciji temelji na povezovanju ali preusmerjanju na pare Presenter:action
. Kaj
pa, če želimo samo izvesti akcijo na trenutni strani? Na primer spremeniti razvrščanje stolpcev v tabeli; izbrisati
element; preklopiti svetel/temen način; poslati obrazec; glasovati v anketi; itd.
Tej vrsti zahtevkov rečemo signali. In podobno kot akcije sprožijo metode action<Action>()
ali
render<Action>()
, signali kličejo metode handle<Signal>()
. Medtem ko je pojem akcije (ali
view) povezan izključno s presenterji, se signali nanašajo na vse komponente. In torej tudi na presenterje, ker je
UI\Presenter
potomec UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... obdelava signala ...
}
Povezavo, ki pokliče signal, ustvarimo na običajen način, torej v predlogi z atributom n:href
ali značko
{link}
, v kodi z metodo link()
. Več v poglavju Ustvarjanje URL povezav.
<a n:href="click! $x, $y">kliknite tukaj</a>
Signal se vedno kliče na trenutnem presenterju in akciji, ni ga mogoče poklicati na drugem presenterju ali drugi akciji.
Signal torej povzroči ponovno nalaganje strani popolnoma enako kot pri prvotnem zahtevku, le da dodatno pokliče obdelovalno metodo signala z ustreznimi parametri. Če metoda ne obstaja, se sproži izjema Nette\Application\UI\BadSignalException, ki se uporabniku prikaže kot stran z napako 403 Forbidden.
Odrezki in AJAX
Signali vas morda nekoliko spominjajo na AJAX: obdelovalci, ki se kličejo na trenutni strani. In imate prav, signali se res pogosto kličejo s pomočjo AJAX-a in nato v brskalnik prenesemo samo spremenjene dele strani. Ali t.i. odrezke. Več informacij najdete na strani, namenjeni AJAX-u.
Flash sporočila
Komponenta ima svoje lastno shrambo flash sporočil, neodvisno od presenterja. Gre za sporočila, ki na primer obveščajo o rezultatu 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.
Pošiljanje zagotavlja metoda flashMessage. Prvi
parameter je besedilo sporočila ali objekt stdClass
, ki predstavlja sporočilo. Neobvezni drugi parameter je njegov
tip (error, warning, info ipd.). Metoda flashMessage()
vrne instanco flash sporočila kot objekt
stdClass
, 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}
Preusmeritev po signalu
Po obdelavi signala komponente pogosto sledi preusmeritev. To je podobna situacija kot pri obrazcih – po njihovem pošiljanju prav tako preusmerjamo, da ob osvežitvi strani v brskalniku ne pride do ponovnega pošiljanja podatkov.
$this->redirect('this'); // preusmeri na trenutni presenter in akcijo
Ker je komponenta ponovno uporaben element in običajno ne bi smela imeti neposredne povezave s konkretnimi presenterji,
metodi redirect()
in link()
samodejno interpretirata parameter kot signal komponente:
$this->redirect('click'); // preusmeri na signal 'click' iste komponente
Če potrebujete preusmeriti na drug presenter ali akcijo, lahko to storite prek presenterja:
$this->getPresenter()->redirect('Product:show'); // preusmeri na drug presenter/akcijo
Persistentni parametri
Persistentni parametri služijo za ohranjanje stanja v komponentah 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, vključno s povezavami, ustvarjenimi v drugih komponentah na isti strani.
Imate na primer komponento za paginacijo vsebine. Takšnih komponent je lahko na strani več. In želimo si, da po kliku na
povezavo ostanejo vse komponente na svoji trenutni strani. Zato iz številke strani (page
) naredimo persistentni
parameter.
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 PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // mora biti public
}
Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. int
) in lahko navedete tudi privzeto vrednost.
Vrednosti parametrov je mogoče validirati.
Pri ustvarjanju povezave lahko persistentnemu parametru spremenite vrednost:
<a n:href="this page: $page + 1">naslednja</a>
Ali pa ga lahko ponastavite, tj. odstranite iz URL-ja. Potem bo prevzel svojo privzeto vrednost:
<a n:href="this page: null">ponastavi</a>
Persistentne komponente
Ne samo parametri, tudi komponente so lahko persistentne. Pri takšni komponenti se njeni persistentni parametri prenašajo
tudi med različnimi akcijami presenterja ali med več presenterji. Persistentne komponente označimo z anotacijo pri razredu
presenterja. Na primer, tako označimo komponente calendar
in poll
:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Podkomponent znotraj teh komponent ni treba označevati, postale bodo persistentne tudi one.
V PHP 8 lahko za označevanje persistentnih komponent uporabite tudi atribute:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponente z odvisnostmi
Kako ustvarjati komponente z odvisnostmi, ne da bi si “onesnažili” presenterje, ki jih bodo uporabljali? Zahvaljujoč pametnim lastnostim DI vsebnika v Nette lahko, enako kot pri uporabi klasičnih storitev, večino dela prepustimo ogrodju.
Vzemimo za primer komponento, ki ima odvisnost od storitve PollFacade
:
class PollControl extends Control
{
public function __construct(
private int $id, // Id ankete, za katero ustvarjamo komponento
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Če bi pisali klasično storitev, ne bi bilo kaj reševati. Za posredovanje vseh odvisnosti bi nevidno poskrbel DI vsebnik.
Vendar pa s komponentami običajno ravnamo tako, da njihovo novo instanco ustvarjamo neposredno v presenterju v tovarniških metodah createComponent…()
. Toda posredovanje vseh odvisnosti vseh
komponent v presenter, da bi jih nato posredovali komponentam, je okorno. In toliko napisane kode…
Logično vprašanje je, zakaj preprosto ne registriramo komponente kot klasične storitve, je ne posredujemo v presenter in
nato v metodi createComponent…()
ne vračamo? Takšen pristop pa je neprimeren, ker želimo imeti možnost
komponento ustvariti tudi večkrat.
Pravilna rešitev je napisati za komponento tovarno, torej razred, ki nam bo komponento ustvaril:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Tako tovarno registriramo v naš vsebnik v konfiguraciji:
services:
- PollControlFactory
in na koncu jo uporabimo v našem presenterju:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // lahko posredujemo naš parameter
return $this->pollControlFactory->create($pollId);
}
}
Odlično je, da Nette DI takšne preproste tovarne zna generirati, tako da namesto njene celotne kode zadostuje napisati samo njen vmesnik:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
In to je vse. Nette notranje ta vmesnik implementira in ga posreduje v presenter, kjer ga že lahko uporabljamo. Čarobno nam
prav v našo komponento doda tudi parameter $id
in instanco razreda PollFacade
.
Komponente v globino
Komponente v Nette Application predstavljajo ponovno uporabne dele spletne aplikacije, ki jih vstavljamo v strani in katerim je posvečeno celotno to poglavje. Kakšne natančno sposobnosti ima takšna komponenta?
- je izrisljiva v predlogi
- ve, kateri svoj del mora izrisati pri AJAX zahtevku (odrezki)
- ima sposobnost shranjevanja svojega stanja v URL (persistentni parametri)
- ima sposobnost odzivanja na uporabniške akcije (signali)
- ustvarja hierarhično strukturo (kjer je koren presenter)
Vsako od teh funkcij zagotavlja kateri od razredov dedne linije. Za izrisovanje (1 + 2) skrbi Nette\Application\UI\Control, za vključitev v življenjski cikel (3, 4) razred Nette\Application\UI\Component in za ustvarjanje hierarhične strukture (5) razreda Container in Component.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Življenjski cikel komponente
Validacija persistentnih parametrov
Vrednosti persistentnih parametrov, prejetih iz URL-ja, zapisuje v lastnosti metoda
loadState()
. Ta tudi preverja, ali ustreza podatkovni tip, naveden pri lastnosti, sicer odgovori z napako 404 in
stran se ne prikaže.
Nikoli slepo ne verjemite persistentnim parametrom, ker jih lahko uporabnik enostavno prepiše v URL-ju. Tako na primer
preverimo, ali je številka strani $this->page
večja od 0. Primerna pot je prepisati omenjeno metodo
loadState()
:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // tukaj se nastavi $this->page
// sledi lastno preverjanje vrednosti:
if ($this->page < 1) {
$this->error();
}
}
}
Nasprotni proces, torej zbiranje vrednosti iz persistentnih lastnosti, ima na skrbi metoda saveState()
.
Signali v globino
Signal povzroči ponovno nalaganje strani popolnoma enako kot pri prvotnem zahtevku (razen v primeru, ko je klican z AJAX-om)
in pokliče metodo signalReceived($signal)
, katere privzeta implementacija v razredu
Nette\Application\UI\Component
poskuša poklicati metodo, sestavljeno iz besed handle{signal}
. Nadaljnja
obdelava je odvisna od danega objekta. Objekti, ki dedujejo od Component
(tzn. Control
in
Presenter
), se odzovejo tako, da poskušajo poklicati metodo handle{signal}
z ustreznimi parametri.
Z drugimi besedami: vzame se definicija funkcije handle{signal}
in vsi parametri, ki so prišli z zahtevkom, ter
se argumentom glede na ime dodelijo parametri iz URL-ja in poskuša poklicati dano metodo. Npr. kot parameter $id
se
posreduje vrednost iz parametra id
v URL-ju, kot $something
se posreduje something
iz
URL-ja itd. In če metoda ne obstaja, metoda signalReceived
sproži izjemo.
Signal lahko sprejme katerakoli komponenta, presenter ali objekt, ki implementira vmesnik SignalReceiver
in je
priključen v drevo komponent.
Med glavne prejemnike signalov bodo spadali Presenterji
in vizualne komponente, ki dedujejo od
Control
. Signal naj bi služil kot znak za objekt, da mora nekaj narediti – anketa si mora zabeležiti glas od
uporabnika, blok z novicami se mora razširiti in prikazati dvakrat toliko novic, obrazec je bil poslan in mora obdelati podatke
in podobno.
URL za signal ustvarimo s pomočjo metode Component::link(). Kot
parameter $destination
posredujemo niz {signal}!
in kot $args
polje argumentov, ki jih
želimo signalu posredovati. Signal se vedno kliče na trenutnem presenterju in akciji s trenutnimi parametri, parametri signala
se samo dodajo. Poleg tega se takoj na začetku doda parameter ?do
, ki določa signal.
Njegov format je bodisi {signal}
ali {signalReceiver}-{signal}
. {signalReceiver}
je ime
komponente v presenterju. Zato v imenu komponente ne sme biti vezaja – uporablja se za ločevanje imena komponente in
signala, vendar je mogoče tako ugnezditi več komponent.
Metoda isSignalReceiver()
preveri, ali je komponenta (prvi argument) prejemnik signala (drugi argument). Drugi argument lahko izpustimo – potem
ugotavlja, ali je komponenta prejemnik kateregakoli signala. Kot drugi parameter lahko navedemo true
in s tem
preverimo, ali je prejemnik ne samo navedena komponenta, ampak tudi katerikoli njen potomec.
V katerikoli fazi pred handle{signal}
lahko signal izvedemo ročno s klicem metode processSignal(), ki
prevzame skrb za obdelavo signala – vzame komponento, ki se je določila kot prejemnik signala (če ni določen prejemnik
signala, je to presenter sam) in ji pošlje signal.
Primer:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
S tem je signal izveden predčasno in se ne bo več ponovno klical.