Interaktív komponensek
A komponensek különálló, újrafelhasználható objektumok, amelyeket oldalakba helyezünk. Ezek lehetnek űrlapok, adattáblák, közvélemény-kutatások, tulajdonképpen bármi, amit érdemes ismételten használni. Megmutatjuk:
- Hogyan használjuk a komponenseket?
- hogyan írjuk meg őket?
- Mik azok a jelek?
A Nette beépített komponensrendszerrel rendelkezik. Az idősebbek talán emlékeznek valami hasonlóra a Delphiből vagy az ASP.NET Web Formsből. A React vagy a Vue.js valami távolról hasonlóra épül. A PHP keretrendszerek világában azonban ez egy teljesen egyedi funkció.
Ugyanakkor a komponensek alapvetően megváltoztatják az alkalmazásfejlesztés megközelítését. Előre elkészített egységekből állíthat össze oldalakat. Adattáblára van szüksége az adminisztrációban? Megtalálhatja a Componette-ben, a Nette nyílt forráskódú kiegészítőinek (nem csak komponensek) tárházában, és egyszerűen beillesztheti a prezenterbe.
Bármennyi komponenst beépíthet a prezenterbe. Néhány komponensbe pedig más komponenseket is beilleszthet. Ezáltal egy komponensfa jön létre, amelynek gyökere a prezenter.
Gyári módszerek
Hogyan kerülnek a komponensek elhelyezésre és későbbi használatra a prezenterben? Általában gyári metódusok segítségével.
A komponens gyár egy elegáns módja annak, hogy csak akkor hozzunk létre komponenseket, amikor valóban szükség van
rájuk (lazy / on-demand). Az egész varázslat egy metódus megvalósításában rejlik, melynek neve
createComponent<Name>()
, ahol <Name>
a komponens neve, amely létrehozza és visszaadja.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Mivel minden komponens külön metódusban jön létre, a kód tisztább és könnyebben olvasható.
A komponensek nevei mindig kisbetűvel kezdődnek, bár a metódusnévben nagybetűvel kezdődnek.
A gyárakat sosem hívjuk közvetlenül, automatikusan meghívódnak, amikor először használjuk a komponenseket. Ennek köszönhetően egy komponens a megfelelő pillanatban jön létre, és csak akkor, ha valóban szükség van rá. Ha nem használnánk a komponenst (például valamilyen AJAX kérésnél, ahol az oldalnak csak egy részét adjuk vissza, vagy amikor a részeket gyorsítótárba helyezzük), akkor nem is jön létre, és ezzel megkíméljük a szerver teljesítményét.
// elérjük a komponenst, és ha ez volt az első alkalom,
// meghívja a createComponentPoll() funkciót a létrehozásához.
$poll = $this->getComponent('poll');
// alternatív szintaxis: $poll = $this['poll'];
A sablonban a {control} címkével renderelhetünk egy komponenst. Így nincs szükség a komponensek manuális átadására a sablonhoz.
<h2>Please Vote</h2>
{control poll}
Hollywood stílus
Az alkatrészek általában egy menő technikát használnak, amit mi hollywoodi stílusnak hívunk. Bizonyára ismered a közhelyet, amit a színészek gyakran hallanak a castingokon: “Ne hívjatok minket, majd mi hívunk titeket”. És erről van szó.
A Nette-ben ahelyett, hogy állandóan kérdéseket tennél fel (“elküldted az űrlapot?”, “érvényes volt?” vagy “megnyomta valaki ezt a gombot?”), azt mondod a keretrendszernek, hogy “ha ez történik, hívd ezt a módszert”, és hagyd rajta a további munkát. Ha JavaScriptben programozol, ismered ezt a programozási stílust. Olyan függvényeket írsz, amelyeket akkor hívsz meg, amikor egy bizonyos esemény bekövetkezik. A motor pedig átadja nekik a megfelelő paramétereket.
Ez teljesen megváltoztatja az alkalmazások írásának módját. Minél több feladatot delegálhatsz a keretrendszerre, annál kevesebb munkád marad. És annál kevesebbet tudsz elfelejteni.
Hogyan írjunk komponenst
A komponens alatt általában a Nette\Application\UI\Control osztály
leszármazottait értjük. Maga a bemutató Nette\Application\UI\Presenter is a
Control
osztály leszármazottja.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
A megjelenítése
Azt már tudjuk, hogy a {control componentName}
címkét egy komponens megrajzolására használjuk. Valójában a
komponens render()
metódusát hívja meg, amelyben a renderelésről gondoskodunk. A prezenterhez hasonlóan van
egy Latte sablonunk a $this->template
változóban,
amelynek átadjuk a paramétereket. A prezenterben való használattal ellentétben itt meg kell adnunk egy sablonfájlt, és
hagynunk kell, hogy renderelje:
public function render(): void
{
// néhány paramétert teszünk a sablonba
$this->template->param = $value;
// és lerajzoljuk
$this->template->render(__DIR__ . '/poll.latte');
}
A {control}
címke segítségével paramétereket adhatunk á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-külön szeretnénk megjeleníteni. Mindegyikhez saját
renderelési metódust hozunk létre, itt van például a renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
És a sablonban aztán meghívjuk a következővel:
{control poll:paginator}
A jobb megértés érdekében jó tudni, hogy a címke hogyan fordítódik PHP kóddá.
{control poll}
{control poll:paginator 123, 'hello'}
Ez a következőre fordítódik:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
getComponent()
poll
komponenst adja vissza, majd a render()
vagy a
renderPaginator()
metódust hívja meg rajta.
Ha a paraméterrészben bárhol =>
használatos, akkor az összes paramétert egy
tömbbe csomagoljuk, és első argumentumként adjuk át:
{control poll, id: 123, message: 'hello'}
fordítja:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Az alkomponens renderelése:
{control cartControl-someForm}
fordít:
$control->getComponent("cartControl-someForm")->render();
A komponensek, például az előadók, automatikusan átadnak számos hasznos változót a sablonoknak:
$basePath
egy abszolút URL elérési útvonal a gyökérkönyvtárhoz (például/CD-collection
).$baseUrl
egy abszolút URL cím a gyökérkönyvtárhoz (pl.http://localhost/CD-collection
)$user
egy objektum, amely a felhasználót képviseli$presenter
az aktuális bemutató$control
az aktuális komponens$flashes
a módszer által küldött üzenetek listájaflashMessage()
Signal
Azt már tudjuk, hogy a Nette alkalmazásban a navigáció a Presenter:action
párokra való hivatkozásból vagy
átirányításból áll. De mi van akkor, ha csak egy műveletet szeretnénk végrehajtani az aktuális oldalon? Például
a táblázat oszlopának rendezési sorrendjét megváltoztatni; elemet törölni; világos/sötét üzemmódot váltani; űrlapot
elküldeni; szavazni a szavazáson; stb.
Az ilyen típusú kérést jelnek nevezzük. És az akciókhoz hasonlóan metódusokat hívnak meg
action<Action>()
vagy render<Action>()
a jelek hívják a módszereket
handle<Signal>()
. Míg az akció (vagy nézet) fogalma csak a prezenterekre vonatkozik, a jelek minden
komponensre. És ezért a prezenterekre is, mivel a UI\Presenter
a UI\Control
leszármazottja.
public function handleClick(int $x, int $y): void
{
// ... a jel feldolgozása ...
}
A jelet hívó link a szokásos módon jön létre, azaz a sablonban a n:href
attribútummal vagy a
{link}
címkével, a kódban a link()
metódussal. Bővebben az URL-linkek létrehozása című fejezetben.
<a n:href="click! $x, $y">click here</a>
A jel mindig az aktuális prezenteren és nézeten kerül meghívásra, így nem lehetséges a jelre más prezenteren/akcióban linkelni.
A jel tehát az oldal újratöltését pontosan ugyanúgy okozza, mint az eredeti kérésnél, csak ezen felül a megfelelő paraméterekkel hívja meg a jelkezelő metódust. Ha a metódus nem létezik, akkor a Nette\Application\UI\BadSignalException kivétel kerül dobásra, amely a felhasználó számára a 403 Forbidden hibalevélként jelenik meg.
Snippetek és AJAX
A jelek egy kicsit emlékeztethetnek az AJAX-ra: az aktuális oldalon meghívott kezelők. És igazad van, a szignálokat valóban gyakran hívjuk meg AJAX segítségével, és akkor csak az oldal megváltozott részeit továbbítjuk a böngészőnek. Ezeket hívják snippeteknek. További információ az AJAX-ről szóló oldalon található.
Flash üzenetek
Egy komponensnek saját, a prezentertől független tárolója van a flash üzeneteknek. Ezek olyan üzenetek, amelyek például a művelet eredményéről tájékoztatnak. A flash-üzenetek fontos jellemzője, hogy a sablonban az átirányítás után is rendelkezésre állnak. A megjelenítés után is még 30 másodpercig életben maradnak – például abban az esetben, ha a felhasználó véletlenül frissítené az oldalt – az üzenet nem vész el.
A küldés a flashMessage metódussal
történik. Az első paraméter az üzenet szövege vagy az üzenetet reprezentáló stdClass
objektum.
A választható második paraméter az üzenet típusa (hiba, figyelmeztetés, info stb.). A flashMessage()
metódus
a flash message egy példányát adja vissza stdClass objektumként, amelyhez információkat adhatunk át.
$this->flashMessage('Az elemet törölték.');
$this->redirect(/* ... */); // és átirányítás
A sablonban ezek az üzenetek a $flashes
változóban állnak rendelkezésre, mint a stdClass
objektumok, 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. Ezeket a következőképpen rajzoljuk meg:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Átirányítás jelzést követően
Egy komponensjel feldolgozása után gyakran következik az átirányítás. Ez a helyzet hasonló az űrlapokhoz – egy űrlap elküldése után mi is átirányítunk, hogy megakadályozzuk az adatok újbóli elküldését, amikor az oldal frissül a böngészőben.
$this->redirect('this') // redirects to the current presenter and action
Mivel a komponens egy újrafelhasználható elem, és általában nem szabad, hogy közvetlen függőségben álljon az egyes
prezenterektől, a redirect()
és a link()
metódusok automatikusan komponensjelként értelmezik a
paramétert:
$this->redirect('click') // redirects to the 'click' signal of the same component
Ha át kell irányítani egy másik prezenterre vagy műveletre, akkor ezt a prezenteren keresztül teheti meg:
$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action
Állandó paraméterek
A tartós paraméterek a komponensek állapotának különböző kérések közötti fenntartására szolgálnak. Értékük a linkre való kattintás után is ugyanaz marad. A munkamenetadatokkal ellentétben ezek az URL-ben kerülnek átvitelre. És automatikusan továbbításra kerülnek, beleértve az ugyanazon az oldalon lévő más komponensekben létrehozott linkeket is.
Például van egy tartalom lapozó komponens. Egy oldalon több ilyen komponens is lehet. És azt szeretné, hogy minden
komponens az aktuális oldalon maradjon, amikor a linkre kattint. Ezért az oldalszámot (page
) állandó
paraméterré tesszük.
A perzisztens paraméter létrehozása rendkívül egyszerű a Nette-ben. Csak hozzon létre egy nyilvános tulajdonságot,
és címkézze meg az 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; // nyilvánosnak kell lennie
}
Javasoljuk, hogy a tulajdonsághoz adja meg az adattípust (pl. int
), és alapértelmezett értéket is megadhat.
A paraméterek értékei érvényesíthetők.
A tartós paraméterek értékét a hivatkozás létrehozásakor módosíthatja:
<a n:href="this page: $page + 1">next</a>
Vagy visszaállítható, azaz eltávolítható az URL-ből. Ekkor az alapértelmezett értéket veszi fel:
<a n:href="this page: null">reset</a>
Tartós komponensek
Nem csak a paraméterek, hanem a komponensek is lehetnek állandóak. A perzisztens paramétereik a különböző műveletek
vagy a különböző előadók között is átvihetők. A perzisztens komponenseket ezzel a megjegyzésekkel jelöljük a
prezenter osztály számára. Itt például a calendar
és a poll
komponenseket jelöljük a
következőképpen:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Az alkomponenseket nem kell tartósnak jelölni, azok automatikusan tartósak.
A PHP 8-ban attribútumokat is használhat a tartós komponensek jelölésére:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Függőségi komponensek
Hogyan hozhatunk létre függőségekkel rendelkező komponenseket anélkül, hogy “összezavarnánk” az őket használó prezentereket? A Nette DI konténerének okos funkcióinak köszönhetően, a hagyományos szolgáltatások használatához hasonlóan, a munka nagy részét a keretrendszerre bízhatjuk.
Vegyünk példaként egy olyan komponenst, amely függőséggel rendelkezik a PollFacade
szolgáltatástól:
class PollControl extends Control
{
public function __construct(
private int $id, // Annak a közvélemény-kutatásnak az azonosítója, amelyhez a komponens létrejön.
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($id, $voteId);
// ...
}
}
Ha egy klasszikus szolgáltatást írnánk, akkor nem kellene aggódnunk. A DI konténer láthatatlanul gondoskodna az összes
függőség átadásáról. De mi általában úgy kezeljük a komponenseket, hogy közvetlenül a prezenterben hozunk létre egy
új példányt belőlük gyári metódusokban createComponent...()
. De az összes
komponens összes függőségének átadása a prezentálónak, hogy aztán átadjuk őket a komponenseknek, nehézkes. És a
megírt kód mennyisége…
A logikus kérdés az, hogy miért nem regisztráljuk a komponenst klasszikus szolgáltatásként, adjuk át a prezenternek,
majd adjuk vissza a createComponent...()
metódusban? De ez a megközelítés nem megfelelő, mert azt szeretnénk,
hogy a komponenst többször is létrehozhassuk.
A helyes megoldás az, ha írunk egy gyárat a komponenshez, azaz egy olyan osztályt, 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);
}
}
Most regisztráljuk a szolgáltatásunkat a DI konténer konfigurációjához:
services:
- PollControlFactory
Végül ezt a gyárat fogjuk használni a prezenterü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);
}
}
A nagyszerű dolog az, hogy a Nette DI képes ilyen egyszerű gyárakat generálni, így az egész kód megírása helyett csak az interfészét kell megírni:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
Ennyi. A Nette belsőleg megvalósítja ezt az interfészt, és befecskendezi a prezenterünkbe, ahol használhatjuk. A
$id
paraméterünket és a PollFacade
osztály példányát is varázslatosan átadja a
komponensünkbe.
A komponensek mélysége
A komponensek egy Nette alkalmazásban a webalkalmazás újrafelhasználható részei, amelyeket oldalakba ágyazunk be, ez a fejezet témája. Pontosan milyen képességekkel rendelkezik egy ilyen komponens?
- egy sablonban megjeleníthető.
- tudja, hogy egy AJAX-kérés során melyik részét kell megjelenítenie (snippetek).
- képes az állapotát egy URL-ben tárolni (állandó paraméterek).
- képes reagálni a felhasználói műveletekre (jelek)
- hierarchikus struktúrát hoz létre (ahol a gyökér a bemutató)
Mindegyik funkciót az öröklési vonal valamelyik osztálya kezeli. A megjelenítést (1 + 2) a Nette\Application\UI\Control, az életciklusba való beillesztést (3, 4) a Nette\Application\UI\Component osztály, a hierarchikus struktúra létrehozását (5) pedig a Container és a Component osztályok kezelik.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
A komponens életciklusa
A komponens életciklusa
A tartós paraméterek validálása
Az URL-ekből kapott állandó paraméterek értékeit a loadState()
módszer írja a tulajdonságokba. A módszer azt is ellenőrzi, hogy a tulajdonsághoz megadott adattípus megfelel-e,
ellenkező esetben 404-es hibával válaszol, és az oldal nem jelenik meg.
Soha ne bízzunk vakon a perzisztens paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Például
így ellenőrizzük, hogy a $this->page
oldalszám nagyobb-e 0-nál. Erre jó megoldás a fent 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 van beállítva a $this->page
// követi a felhasználói értékek ellenőrzését:
if ($this->page < 1) {
$this->error();
}
}
}
Az ellenkező folyamatot, vagyis az értékek begyűjtését a perzisztens tulajdonságokból a saveState()
metódus kezeli.
Mélységi jelek
A jel az eredeti kéréshez hasonlóan az oldal újratöltését okozza (az AJAX kivételével), és meghívja a
signalReceived($signal)
metódust, amelynek alapértelmezett megvalósítása a
Nette\Application\UI\Component
osztályban a handle{Signal}
szavakból álló metódust próbálja
meghívni. A további feldolgozás az adott objektumra támaszkodik. A Component
leszármazottai (azaz a
Control
és a Presenter
) objektumok a handle{Signal}
metódust próbálják meghívni a
megfelelő paraméterekkel.
Más szóval: a handle{Signal}
metódus definícióját vesszük, és a kérésben kapott összes paramétert
összevetjük a metódus paramétereivel. Ez azt jelenti, hogy a id
paramétert az URL-ből a $id
, a
something
a $something
és így tovább. Ha pedig a módszer nem létezik, a signalReceived
módszer kivételt dob.
A jelet bármely komponens, bemutató objektum, amely a SignalReceiver
interfészt valósítja meg, fogadhatja,
ha csatlakozik a komponensfához.
A jelek fő vevői a Presenters
és a Control
kiterjesztő vizuális komponensek. A jel egy
objektum számára jelzi, hogy valamit tennie kell – a felhasználó szavazatát számolja a szavazás, a híreket tartalmazó
doboznak ki kell bontakoznia, az űrlapot elküldték és fel kell dolgoznia az adatokat, és így tovább.
A jel URL-címét a Component::link() metódus
segítségével hozzuk létre. A $destination
paraméterként a {signal}!
stringet adjuk át, a
$args
pedig egy tömbnyi argumentumot, amelyet a jelkezelőnek szeretnénk átadni. A jelparamétereket az aktuális
prezenter/nézet URL-hez csatoljuk. A ?do
paraméter az URL-ben meghatározza a hívott jelet.
Formátuma {signal}
vagy {signalReceiver}-{signal}
. {signalReceiver}
a prezenterben
lévő komponens neve. Ezért nem lehet kötőjel (pontatlanul kötőjel) a komponensek nevében – ez a komponens és a jel
nevének elválasztására szolgál, de több komponens is összeállítható.
Az isSignalReceiver()
metódus ellenőrzi, hogy egy komponens (első argumentum) egy jel (második argumentum) vevője-e. A második argumentum
elhagyható – ekkor kiderül, hogy a komponens bármely jel vevője-e. Ha a második paraméter a true
, akkor
ellenőrzi, hogy a komponens vagy annak leszármazottai egy jel vevői-e.
Bármelyik fázisban a handle{Signal}
megelőzően a processSignal()
metódus meghívásával manuálisan is lehet jelet végrehajtani, amely a jelek végrehajtásáért vállal felelősséget.
Átveszi a vevő komponenst (ha nincs beállítva, akkor maga a prezenter) és elküldi neki a jelet.
Példa:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
A jelet idő előtt végrehajtják, és nem hívják meg újra.