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:
$basePath
az abszolút URL elérési út a gyökérkönyvtárhoz (pl./eshop
)$baseUrl
az abszolút URL a gyökérkönyvtárhoz (pl.http://localhost/eshop
)$user
a felhasználót reprezentáló objektum$presenter
az aktuális presenter$control
az aktuális komponens$flashes
aflashMessage()
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.
Snippets é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. Snippet-eket. További információkat az AJAX-nak szentelt oldalon talál.
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($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 (Snippets)
- 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.