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}
Állandó paraméterek
Gyakran van szükség arra, hogy egy komponensben valamilyen paramétert a komponenssel való munka teljes ideje alatt
megőrizzünk. Ez lehet például az oldalszám a lapozásban. Ezt a paramétert a @persistent
megjegyzéssel
állandónak kell jelölni.
class PollControl extends Control
{
/** @persistent */
public $page = 1;
}
Ez a paraméter automatikusan átadásra kerül minden linkben a GET
paraméterként, amíg a felhasználó el nem
hagyja az oldalt ezzel a komponenssel.
Soha ne bízzon vakon a tartós paraméterekben, mert könnyen meghamisíthatók (az URL felülírásával). Ellenőrizze például, hogy az oldalszám a megfelelő intervallumon belül van-e.
A PHP 8-ban attribútumokat is használhat a perzisztens paraméterek jelölésére:
use Nette\Application\Attributes\Persistent;
class PollControl extends Control
{
#[Persistent]
public $page = 1;
}
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\UI\Application\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 renderelni (snippet)
- képes az állapotát egy URL-ben tárolni (perzisztencia 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
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.