Interaktív komponensek
A komponensek önálló, újrafelhasználható objektumok, amelyeket oldalakba illesztünk be. Lehetnek űrlapok, datagrid-ek, szavazások, valójában bármi, amit érdemes ismételten használni. Megmutatjuk:
- hogyan használjuk a komponenseket?
- hogyan írjunk komponenseket?
- mik azok a signálok?
A Nette beépített komponensrendszerrel rendelkezik. Valami hasonlót a Delphi vagy az ASP.NET Web Forms ismerői ismerhetnek, valami távolról hasonlóra épül a React vagy a Vue.js is. Azonban a PHP keretrendszerek világában ez egyedülálló dolog.
Eközben a komponensek alapvetően befolyásolják az alkalmazásfejlesztési megközelítést. Az oldalakat előre elkészített egységekből állíthatja össze. Szüksége van egy datagridre az adminisztrációban? Megtalálja a Componette oldalon, amely a Nette nyílt forráskódú kiegészítőinek (tehát nem csak komponenseknek) a tárolója, és egyszerűen beillesztheti a presenterbe.
A presenterbe tetszőleges számú komponenst beépíthet. És néhány komponensbe további komponenseket is beilleszthet. Így egy komponensfa jön létre, amelynek gyökere a presenter.
Factory metódusok
Hogyan illesztjük be és használjuk a komponenseket a presenterben? Általában factory metódusok segítségével.
A komponens factory elegáns módja annak, hogy a komponenseket csak akkor hozzuk létre, amikor valóban szükség van rájuk
(lazy / on demand). Az egész varázslat egy createComponent<Name>() nevű metódus implementálásában
rejlik, ahol <Name> a létrehozandó komponens neve, és amely létrehozza és visszaadja a komponenst.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Annak köszönhetően, hogy minden komponens külön metódusban jön létre, a kód áttekinthetőbbé válik.
A komponensek nevei mindig kisbetűvel kezdődnek, annak ellenére, hogy a metódus nevében nagybetűvel íródnak.
A factory-kat soha nem hívjuk meg közvetlenül, maguktól hívódnak meg, amikor először használjuk a komponenst. Ennek köszönhetően a komponens a megfelelő pillanatban jön létre, és csak akkor, ha valóban szükség van rá. Ha nem használjuk a komponenst (például egy AJAX kérésnél, amikor csak az oldal egy része kerül átvitelre, vagy a sablon cache-elésekor), egyáltalán nem jön létre, és megspóroljuk a szerver teljesítményét.
// hozzáférünk a komponenshez, és ha ez volt az első alkalom,
// meghívódik a createComponentPoll(), amely létrehozza
$poll = $this->getComponent('poll');
// alternatív szintaxis: $poll = $this['poll'];
A sablonban a komponenst a {control} tag segítségével lehet renderelni. Ezért nincs szükség a komponensek manuális átadására a sablonnak.
<h2>Szavazzon</h2>
{control poll}
Hollywood style
A komponensek általában egy friss technikát használnak, amit szeretünk Hollywood style-nak nevezni. Biztosan ismeri a szállóigévé vált mondatot, amit a filmes meghallgatások résztvevői oly gyakran hallanak: „Ne hívjon minket, mi majd hívjuk önt”. És pontosan erről van szó.
A Nette-ben ugyanis ahelyett, hogy állandóan kérdezgetnie kellene („elküldték az űrlapot?”, „érvényes volt?” vagy „megnyomta a felhasználó ezt a gombot?”), azt mondja a keretrendszernek, „amikor ez megtörténik, hívd meg ezt a metódust”, és a további munkát ráhagyja. Ha JavaScriptben programozik, ezt a programozási stílust jól ismeri. Olyan függvényeket ír, amelyek akkor hívódnak meg, amikor egy bizonyos esemény bekövetkezik. És a nyelv átadja nekik a megfelelő paramétereket.
Ez teljesen megváltoztatja az alkalmazások írásáról alkotott képet. Minél több feladatot bízhat a keretrendszerre, annál kevesebb munkája van Önnek. És annál kevesebb dolgot hagyhat ki esetleg.
Komponens írása
Komponens alatt általában a Nette\Application\UI\Control osztály
leszármazottját értjük. (Pontosabb lenne tehát a „controls” kifejezést használni, de a „kontrolloknak” a magyarban
teljesen más jelentése van, és inkább a „komponensek” terjedtek el.) Maga a presenter Nette\Application\UI\Presenter egyébként
szintén a Control osztály leszármazottja.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Renderelés
Már tudjuk, hogy a komponens renderelésére a {control componentName} tag szolgál. Ez valójában a komponens
render() metódusát hívja meg, amelyben gondoskodunk a renderelésről. Rendelkezésünkre áll, ugyanúgy, mint a
presenterben, egy Latte sablon a $this->template
változóban, amelynek paramétereket adunk át. A presentertől eltérően itt meg kell adnunk a sablonfájlt, és hagynunk
kell, hogy renderelje:
public function render(): void
{
// beillesztünk néhány paramétert a sablonba
$this->template->param = $value;
// és rendereljük
$this->template->render(__DIR__ . '/poll.latte');
}
A {control} tag lehetővé teszi paraméterek átadását a render() metódusnak:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Néha egy komponens több részből állhat, amelyeket külön szeretnénk renderelni. Mindegyikhez létrehozunk egy saját
renderelő metódust, itt a példában például renderPaginator():
public function renderPaginator(): void
{
// ...
}
És a sablonban ezt a következőképpen hívjuk meg:
{control poll:paginator}
A jobb megértés érdekében jó tudni, hogyan fordítódik le ez a tag PHP-ra.
{control poll}
{control poll:paginator 123, 'hello'}
lefordítva:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
A getComponent() metódus visszaadja a poll komponenst, és ezen a komponensen hívja meg a
render() metódust, illetve a renderPaginator() metódust, ha a tag-ben a kettőspont után más
renderelési mód van megadva.
Figyelem, ha bárhol a paraméterek között => jelenik meg, az összes paraméter egy
tömbbe lesz csomagolva és az első argumentumként kerül átadásra:
{control poll, id: 123, message: 'hello'}
lefordítva:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Alkomponens renderelése:
{control cartControl-someForm}
lefordítva:
$control->getComponent("cartControl-someForm")->render();
A komponensek, akárcsak a presenterek, automatikusan átadnak néhány hasznos változót a sablonoknak:
$basePathaz abszolút URL elérési út a gyökérkönyvtárhoz (pl./eshop)$baseUrlaz abszolút URL a gyökérkönyvtárhoz (pl.http://localhost/eshop)$usera felhasználót reprezentáló objektum$presenteraz aktuális presenter$controlaz aktuális komponens$flashesaflashMessage()függvénnyel küldött üzenetek tömbje
Signal
Már tudjuk, hogy a Nette alkalmazásban a navigáció linkekre vagy átirányításokra épül Presenter:action
párokra. De mi van akkor, ha csak egy műveletet szeretnénk végrehajtani az aktuális oldalon? Például megváltoztatni
az oszlopok sorrendjét egy táblázatban; törölni egy elemet; váltani világos/sötét mód között; elküldeni egy űrlapot;
szavazni egy szavazáson; stb.
Az ilyen típusú kéréseket signáloknak nevezzük. És ahogy az akciók a action<Action>() vagy
render<Action>() metódusokat hívják meg, a signálok a handle<Signal>() metódusokat
hívják meg. Míg az akció (vagy view) fogalma tisztán csak a presenterekhez kapcsolódik, a signálok minden komponensre
vonatkoznak. És így a presenterekre is, mivel az UI\Presenter az UI\Control leszármazottja.
public function handleClick(int $x, int $y): void
{
// ... signál feldolgozása ...
}
A signált meghívó linket a szokásos módon hozzuk létre, azaz a sablonban az n:href attribútummal vagy a
{link} taggel, a kódban pedig a link() metódussal. További információk az URL linkek létrehozása fejezetben.
<a n:href="click! $x, $y">kattints ide</a>
A signál mindig az aktuális presenteren és action-ön hívódik meg, nem lehet másik presenteren vagy másik action-ön meghívni.
A signál tehát az oldal újratöltését okozza, ugyanúgy, mint az eredeti kérésnél, csak emellett meghívja a signál kezelő metódusát a megfelelő paraméterekkel. Ha a metódus nem létezik, Nette\Application\UI\BadSignalException kivétel dobódik, amely a felhasználónak 403 Forbidden hibaoldalként jelenik meg.
Snippetek és AJAX
A signálok talán egy kicsit emlékeztetnek az AJAX-ra: handlerek, amelyek az aktuális oldalon hívódnak meg. És igaza van, a signálokat valóban gyakran AJAX segítségével hívják meg, és utána csak az oldal megváltozott részeit továbbítjuk a böngészőbe. Vagyis az ún. snippeteket. További információkat talál az AJAX-nak szentelt oldalon.
Flash üzenetek
A komponensnek saját flash üzenet tárolója van, amely független a presentertől. Ezek olyan üzenetek, amelyek pl. egy művelet eredményéről tájékoztatnak. A flash üzenetek fontos jellemzője, hogy a sablonban átirányítás után is elérhetők. Megjelenítésük után még további 30 másodpercig élnek – például arra az esetre, ha a felhasználó hibás átvitel miatt frissítené az oldalt – az üzenet tehát nem tűnik el azonnal.
A küldést a flashMessage metódus
végzi. Az első paraméter az üzenet szövege vagy egy stdClass objektum, amely az üzenetet reprezentálja. A nem
kötelező második paraméter a típusa (error, warning, info stb.). A flashMessage() metódus visszaadja a flash
üzenet példányát stdClass objektumként, amelyhez további információkat lehet hozzáadni.
$this->flashMessage('Az elem törölve lett.');
$this->redirect(/* ... */); // és átirányítunk
A sablonban ezek az üzenetek a $flashes változóban érhetők el stdClass objektumokként, amelyek
tartalmazzák a message (üzenet szövege), type (üzenet típusa) tulajdonságokat, és
tartalmazhatják a már említett felhasználói információkat is. Például így rendereljük őket:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Átirányítás signál után
A komponensek signáljának feldolgozása után gyakran átirányítás következik. Ez hasonló helyzet, mint az űrlapoknál – elküldésük után is átirányítunk, hogy a böngészőben az oldal frissítésekor ne küldődjenek újra az adatok.
$this->redirect('this') // átirányít az aktuális presenter-re és action-re
Mivel a komponens egy újrafelhasználható elem, és általában nem kellene, hogy közvetlen kapcsolata legyen konkrét
presenterekkel, a redirect() és link() metódusok automatikusan komponens signálként értelmezik a
paramétert:
$this->redirect('click') // átirányít ugyanazon komponens 'click' signáljára
Ha másik presenter-re vagy akcióra kell átirányítani, ezt a presenteren keresztül teheti meg:
$this->getPresenter()->redirect('Product:show'); // átirányít másik presenter/action-re
Perzisztens paraméterek
A perzisztens paraméterek a komponensek állapotának megőrzésére szolgálnak a különböző kérések között. Értékük ugyanaz marad a linkre kattintás után is. A session adatokkal ellentétben az URL-ben kerülnek átvitelre. És ez teljesen automatikusan történik, beleértve az ugyanazon az oldalon lévő más komponensekben létrehozott linkeket is.
Például van egy komponensünk a tartalom lapozásához. Ilyen komponensekből több is lehet az oldalon. És azt szeretnénk,
hogy egy linkre kattintás után minden komponens az aktuális oldalán maradjon. Ezért az oldalszámból (page)
perzisztens paramétert csinálunk.
Perzisztens paraméter létrehozása a Nette-ben rendkívül egyszerű. Csak létre kell hozni egy public property-t és
megjelölni egy attribútummal: (korábban a /** @persistent */ volt használatos)
use Nette\Application\Attributes\Persistent; // ez a sor fontos
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // public-nak kell lennie
}
A property-nél javasoljuk az adattípus megadását (pl. int), és megadhat alapértelmezett értéket is.
A paraméterek értékeit lehet validálni.
Link létrehozásakor a perzisztens paraméter értékét meg lehet változtatni:
<a n:href="this page: $page + 1">következő</a>
Vagy resetelhető, azaz eltávolítható az URL-ből. Ekkor az alapértelmezett értékét veszi fel:
<a n:href="this page: null">reset</a>
Perzisztens komponensek
Nemcsak a paraméterek, hanem a komponensek is lehetnek perzisztensek. Egy ilyen komponens perzisztens paraméterei
átkerülnek a presenter különböző akciói között vagy több presenter között is. A perzisztens komponenseket
annotációval jelöljük a presenter osztályánál. Például így jelöljük a calendar és poll
komponenseket:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Az ezekben a komponensekben lévő alkomponenseket nem kell jelölni, azok is perzisztensekké válnak.
PHP 8-ban attribútumokat is használhat a perzisztens komponensek jelölésére:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponensek függőségekkel
Hogyan hozzunk létre komponenseket függőségekkel anélkül, hogy „beszennyeznénk” azokat a presentereket, amelyek használni fogják őket? A Nette DI konténerének okos tulajdonságainak köszönhetően, ugyanúgy, mint a klasszikus szolgáltatások használatakor, a munka nagy részét a keretrendszerre bízhatjuk.
Vegyünk példaként egy komponenst, amelynek függősége van a PollFacade szolgáltatásra:
class PollControl extends Control
{
public function __construct(
private int $id, // Annak a szavazásnak az ID-ja, amelyhez komponenst hozunk létre
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Ha klasszikus szolgáltatást írnánk, nem lenne mit megoldani. Az összes függőség átadásáról láthatatlanul
gondoskodna a DI konténer. De a komponensekkel általában úgy bánunk, hogy új példányukat közvetlenül a presenterben
hozzuk létre a factory metódusokban createComponent…(). De az összes
komponens összes függőségét átadni a presenternek, hogy aztán átadjuk a komponenseknek, nehézkes. És mennyi
írott kód…
A logikus kérdés az, hogy miért nem regisztráljuk egyszerűen a komponenst klasszikus szolgáltatásként, adjuk át a
presenternek, majd a createComponent…() metódusban adjuk vissza? Ez a megközelítés azonban nem megfelelő, mert
a komponenst akár többször is szeretnénk létrehozni.
A helyes megoldás egy factory írása a komponenshez, azaz egy osztály, amely létrehozza nekünk a komponenst:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Így regisztráljuk a factory-t a konténerünkbe a konfigurációban:
services:
- PollControlFactory
és végül használjuk a presenterünkben:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // átadhatjuk a paraméterünket
return $this->pollControlFactory->create($pollId);
}
}
Nagyszerű, hogy a Nette DI ilyen egyszerű factory-kat tud generálni, így a teljes kód helyett elegendő csak az interfészét megírni:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
És ez minden. A Nette belsőleg implementálja ezt az interfészt és átadja a presenternek, ahol már használhatjuk is.
Mágikusan hozzáadja a komponensünkhöz az $id paramétert és a PollFacade osztály
példányát is.
Komponensek mélységében
A Nette Application komponensei újrafelhasználható részei a webalkalmazásnak, amelyeket oldalakba illesztünk, és amelyekkel egyébként ez az egész fejezet foglalkozik. Milyen képességekkel rendelkezik pontosan egy ilyen komponens?
- renderelhető a sablonban
- tudja, melyik részét kell renderelni AJAX kérés esetén (snippetek)
- képes az állapotát az URL-ben tárolni (perzisztens paraméterek)
- képes reagálni a felhasználói műveletekre (signálok)
- hierarchikus struktúrát hoz létre (ahol a gyökér a presenter)
Ezeknek a funkcióknak mindegyikét az öröklési lánc valamelyik osztálya látja el. A renderelést (1 + 2) a Nette\Application\UI\Control osztály intézi, az életciklusba való beilleszkedést (3, 4) a Nette\Application\UI\Component osztály, a hierachikus struktúra létrehozását (5) pedig a Container és Component osztályok:
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Komponens életciklusa
Perzisztens paraméterek validálása
Az URL-ből kapott perzisztens paraméterek értékeit a loadState()
metódus írja be a property-kbe. Ez ellenőrzi azt is, hogy megfelelnek-e a property-nél megadott adattípusnak, különben
404-es hibával válaszol, és az oldal nem jelenik meg.
Soha ne bízzon vakon a perzisztens paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Így
például ellenőrizzük, hogy az oldalszám $this->page nagyobb-e 0-nál. Megfelelő módszer az említett
loadState() metódus felülírása:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // itt állítódik be a $this->page
// következik a saját értékellenőrzés:
if ($this->page < 1) {
$this->error();
}
}
}
Az ellenkező folyamatot, azaz az értékek összegyűjtését a perzisztens property-kből, a saveState()
metódus végzi.
Signálok mélységében
A signál az oldal újratöltését okozza, ugyanúgy, mint az eredeti kérésnél (kivéve, ha AJAX-szal hívják), és
meghívja a signalReceived($signal) metódust, amelynek alapértelmezett implementációja a
Nette\Application\UI\Component osztályban megpróbál meghívni egy handle{signal} szavakból
összetett metódust. A további feldolgozás az adott objektumon múlik. A Component-től öröklődő objektumok
(azaz a Control és a Presenter) úgy reagálnak, hogy megpróbálják meghívni a
handle{signal} metódust a megfelelő paraméterekkel.
Más szavakkal: veszi a handle{signal} függvény definícióját és az összes paramétert, amely a kéréssel
érkezett, és az argumentumokhoz név szerint hozzárendeli az URL paramétereit, majd megpróbálja meghívni az adott
metódust. Például az $id paraméterként az URL id paraméterének értékét adja át, a
$something paraméterként az URL something értékét adja át, stb. És ha a metódus nem létezik, a
signalReceived metódus kivételt dob.
Signált bármely komponens, presenter vagy objektum fogadhat, amely implementálja a SignalReceiver interfészt
és csatlakozik a komponensfához.
A signálok fő fogadói a Presenterek és a Control-tól öröklődő vizuális komponensek
lesznek. A signál jelzésként szolgál az objektum számára, hogy tegyen valamit – a szavazás számolja be a felhasználó
szavazatát, a hírek blokkja bontakozzon ki és jelenítsen meg kétszer annyi hírt, az űrlap elküldésre került és dolgozza
fel az adatokat, és így tovább.
A signál URL-jét a Component::link() metódussal
hozzuk létre. A $destination paraméterként adjuk át a {signal}! stringet, a $args
paraméterként pedig az argumentumok tömbjét, amelyeket a signálnak szeretnénk átadni. A signál mindig az aktuális
presenteren és action-ön hívódik meg az aktuális paraméterekkel, a signál paraméterei csak hozzáadódnak. Ezenkívül
rögtön az elején hozzáadódik a ?do paraméter, amely meghatározza a signált.
Formátuma vagy {signal}, vagy {signalReceiver}-{signal}. A {signalReceiver} a komponens
neve a presenterben. Ezért nem lehet kötőjel a komponens nevében – a komponens nevének és a signálnak az
elválasztására szolgál, azonban így több komponenst is be lehet ágyazni.
A isSignalReceiver()
metódus ellenőrzi, hogy a komponens (első argumentum) a signál (második argumentum) fogadója-e. A második argumentumot
elhagyhatjuk – ekkor azt vizsgálja, hogy a komponens bármilyen signál fogadója-e. Második paraméterként megadhatunk
true-t, és ezzel ellenőrizhetjük, hogy nemcsak a megadott komponens a fogadó, hanem bármelyik
leszármazottja is.
Bármely, a handle{signal} előtti fázisban manuálisan végrehajthatjuk a signált a processSignal()
metódus meghívásával, amely gondoskodik a signál elintézéséről – veszi a signál fogadójaként meghatározott
komponenst (ha nincs megadva signál fogadó, akkor maga a presenter az) és elküldi neki a signált.
Példa:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Ezzel a signál idő előtt végrehajtódik, és nem fog újra meghívódni.