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

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?

  1. egy sablonban megjeleníthető.
  2. tudja, hogy egy AJAX-kérés során melyik részét kell renderelni (snippet)
  3. képes az állapotát egy URL-ben tárolni (perzisztencia 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

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