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ája flashMessage()

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

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\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?

  1. egy sablonban megjeleníthető.
  2. tudja, hogy egy AJAX-kérés során melyik részét kell megjelenítenie (snippetek).
  3. képes az állapotát egy URL-ben tárolni (állandó paraméterek).
  4. képes reagálni a felhasználói műveletekre (jelek)
  5. 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.

verzió: 4.0