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 funkcijo flashMessage()

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?

  1. je izrisljiva v predlogi
  2. ve, kateri svoj del mora izrisati pri AJAX zahtevku (odrezki)
  3. ima sposobnost shranjevanja svojega stanja v URL (persistentni parametri)
  4. ima sposobnost odzivanja na uporabniške akcije (signali)
  5. 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

Ž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.

različica: 4.0