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 a flashMessage() 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?

  1. renderelhető a sablonban
  2. tudja, melyik részét kell renderelni AJAX kérés esetén (Snippets)
  3. képes az állapotát az URL-ben tárolni (perzisztens paraméterek)
  4. képes reagálni a felhasználói műveletekre (signálok)
  5. 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

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.

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.

verzió: 4.0