Űrlapok presenterekben

A Nette Forms rendkívül megkönnyíti a webes űrlapok létrehozását és feldolgozását. Ebben a fejezetben megismerkedhet az űrlapok használatával a presentereken belül.

Ha érdekli, hogyan használhatja őket teljesen önállóan a keretrendszer többi része nélkül, akkor az önálló használat útmutatója Önnek szól.

Első űrlap

Próbáljunk meg írni egy egyszerű regisztrációs űrlapot. A kódja a következő lesz:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Név:');
$form->addPassword('password', 'Jelszó:');
$form->addSubmit('send', 'Regisztráció');
$form->onSuccess[] = [$this, 'formSucceeded'];

és a böngészőben így jelenik meg:

Az űrlap a presenterben egy Nette\Application\UI\Form osztály objektuma, elődje, a Nette\Forms\Form önálló használatra készült. Hozzáadtunk ún. név, jelszó elemeket és egy küldés gombot. Végül a $form->onSuccess sor azt mondja, hogy elküldés és sikeres validálás után meg kell hívni a $this->formSucceeded() metódust.

A presenter szempontjából az űrlap egy szokásos komponens. Ezért komponensként kezeljük, és a presenterbe egy factory metódus segítségével illesztjük be. Ez így fog kinézni:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Név:');
		$form->addPassword('password', 'Jelszó:');
		$form->addSubmit('send', 'Regisztráció');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// itt dolgozzuk fel az űrlappal küldött adatokat
		// $data->name tartalmazza a nevet
		// $data->password tartalmazza a jelszót
		$this->flashMessage('Sikeresen regisztrált.');
		$this->redirect('Home:');
	}
}

És a sablonban az űrlapot a {control} taggel jelenítjük meg:

<h1>Regisztráció</h1>

{control registrationForm}

És ez tulajdonképpen minden :-) Van egy működő és tökéletesen biztonságos űrlapunk.

És most valószínűleg azt gondolja, hogy ez túl gyors volt, azon tűnődik, hogyan lehetséges, hogy meghívódik a formSucceeded() metódus, és mik azok a paraméterek, amelyeket kap. Persze, igaza van, ez magyarázatot érdemel.

A Nette ugyanis egy friss mechanizmussal érkezik, amelyet Hollywood style-nak nevezünk. Ahelyett, hogy fejlesztőként állandóan kérdezgetnie kellene, hogy történt-e valami („el lett küldve az űrlap?”, „érvényesen lett elküldve?” és „nem hamisították-e meg?”), azt mondja a keretrendszernek: „amikor az űrlap érvényesen ki van töltve, 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. Függvényeket ír, amelyek akkor hívódnak meg, amikor egy bizonyos esemény bekövetkezik. És a nyelv átadja nekik a megfelelő argumentumokat.

Pontosan így épül fel a fenti presenter kód is. A $form->onSuccess tömb PHP callbackek listáját képviseli, amelyeket a Nette akkor hív meg, amikor az űrlap elküldésre kerül és helyesen van kitöltve (azaz érvényes). A presenter életciklusa keretében ez egy ún. signal, tehát az action* metódus után és a render* metódus előtt hívódnak meg. És minden callbacknek átadja első paraméterként magát az űrlapot, második paraméterként pedig az elküldött adatokat egy ArrayHash objektum formájában. Az első paramétert kihagyhatja, ha nincs szüksége az űrlap objektumra. A második paraméter pedig lehet okosabb, de erről majd később.

A $data objektum tartalmazza a name és password kulcsokat azokkal az adatokkal, amelyeket a felhasználó kitöltött. Általában az adatokat azonnal továbbítjuk további feldolgozásra, ami lehet például adatbázisba való beszúrás. A feldolgozás során azonban hiba léphet fel, például a felhasználónév már foglalt. Ebben az esetben a hibát visszaküldjük az űrlapnak az addError() segítségével, és hagyjuk újra megjeleníteni, a hibaüzenettel együtt.

$form->addError('Sajnáljuk, a felhasználónév már foglalt.');

Az onSuccess mellett létezik még az onSubmit: a callbackek mindig az űrlap elküldése után hívódnak meg, akkor is, ha nincs helyesen kitöltve. Továbbá az onError: a callbackek csak akkor hívódnak meg, ha az elküldés nem érvényes. Akkor is meghívódnak, ha az onSuccess vagy onSubmit során érvénytelenítjük az űrlapot az addError() segítségével.

Az űrlap feldolgozása után átirányítunk a következő oldalra. Ez megakadályozza az űrlap nem kívánt újraküldését a frissítés, vissza gombbal vagy a böngésző előzményeiben való mozgással.

Próbáljon meg hozzáadni további űrlap elemeket is.

Elemekhez való hozzáférés

Az űrlap a presenter komponense, esetünkben registrationForm néven (a createComponentRegistrationForm factory metódus neve alapján), így bárhol a presenterben hozzáférhet az űrlaphoz a következőképpen:

$form = $this->getComponent('registrationForm');
// alternatív szintaxis: $form = $this['registrationForm'];

Az egyes űrlap elemek is komponensek, ezért ugyanúgy hozzáférhet hozzájuk:

$input = $form->getComponent('name'); // vagy $input = $form['name'];
$button = $form->getComponent('send'); // vagy $button = $form['send'];

Az elemeket az unset segítségével távolíthatja el:

unset($form['name']);

Validálási szabályok

Elhangzott a valid szó, de az űrlapnak még nincsenek validálási szabályai. Javítsuk ezt ki.

A név kötelező lesz, ezért megjelöljük a setRequired() metódussal, amelynek argumentuma a hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki a nevet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra.

$form->addText('name', 'Név:')
	->setRequired('Kérjük, adja meg a nevét');

Próbálja meg elküldeni az űrlapot kitöltetlen névvel, és látni fogja, hogy megjelenik a hibaüzenet, és a böngésző vagy a szerver elutasítja, amíg ki nem tölti a mezőt.

Ugyanakkor a rendszert nem csaphatja be azzal, hogy a mezőbe például csak szóközöket ír. Dehogy. A Nette automatikusan eltávolítja a bal- és jobboldali szóközöket. Próbálja ki. Ez egy olyan dolog, amit minden egysoros inputtal mindig meg kellene tennie, de gyakran elfelejtik. A Nette ezt automatikusan megteszi. (Megpróbálhatja becsapni az űrlapot, és névként többsoros stringet küldeni. A Nette itt sem hagyja magát megtéveszteni, és az újsorokat szóközökre cseréli.)

Az űrlap mindig a szerveroldalon validálódik, de JavaScript validáció is generálódik, amely villámgyorsan lefut, és a felhasználó azonnal értesül a hibáról, anélkül, hogy az űrlapot el kellene küldenie a szerverre. Ezt a netteForms.js szkript végzi. Illessze be a layout sablonba:

<script src="https://unpkg.com/nette-forms@3"></script>

Ha megnézi az űrlapot tartalmazó oldal forráskódját, észreveheti, hogy a Nette a kötelező elemeket required CSS osztállyal rendelkező elemekbe helyezi. Próbálja meg hozzáadni a következő stíluslapot a sablonhoz, és a „Név” címke piros lesz. Elegánsan jelöljük így a felhasználóknak a kötelező elemeket:

<style>
.required label { color: maroon }
</style>

További validálási szabályokat az addRule() metódussal adunk hozzá. Az első paraméter a szabály, a második ismét a hibaüzenet szövege, és még következhet a validálási szabály argumentuma. Mit jelent ez?

Az űrlapot kibővítjük egy új, nem kötelező „életkor” mezővel, amelynek egész számnak kell lennie (addInteger()), és ráadásul a megengedett tartományban ($form::Range). És itt pontosan kihasználjuk az addRule() metódus harmadik paraméterét, amellyel átadjuk a validátornak a kívánt tartományt egy [tól, ig] párként:

$form->addInteger('age', 'Életkor:')
	->addRule($form::Range, 'Az életkornak 18 és 120 között kell lennie', [18, 120]);

Ha a felhasználó nem tölti ki a mezőt, a validálási szabályok nem kerülnek ellenőrzésre, mivel az elem nem kötelező.

Itt van lehetőség egy kis refaktorálásra. A hibaüzenetben és a harmadik paraméterben a számok duplikáltan szerepelnek, ami nem ideális. Ha többnyelvű űrlapokat hoznánk létre, és a számokat tartalmazó üzenet több nyelvre lenne lefordítva, megnehezítené az értékek esetleges megváltoztatását. Ebből az okból kifolyólag használhatók a %d helyettesítő karakterek, és a Nette kiegészíti az értékeket:

	->addRule($form::Range, 'Az életkornak %d és %d év között kell lennie', [18, 120]);

Térjünk vissza a password elemhez, amelyet szintén kötelezővé teszünk, és még ellenőrizzük a jelszó minimális hosszát ($form::MinLength), ismét a helyettesítő karakter használatával:

$form->addPassword('password', 'Jelszó:')
	->setRequired('Válasszon jelszót')
	->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8);

Hozzáadunk az űrlaphoz még egy passwordVerify mezőt, ahol a felhasználó még egyszer megadja a jelszót, ellenőrzés céljából. Validálási szabályokkal ellenőrizzük, hogy mindkét jelszó azonos-e ($form::Equal). És paraméterként hivatkozást adunk az első jelszóra a szögletes zárójelek segítségével:

$form->addPassword('passwordVerify', 'Jelszó újra:')
	->setRequired('Kérjük, adja meg a jelszót újra az ellenőrzéshez')
	->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password'])
	->setOmitted();

A setOmitted() segítségével megjelöltük azt az elemet, amelynek az értékére valójában nem vagyunk kíváncsiak, és amely csak a validáció miatt létezik. Az érték nem kerül átadásra a $data-ba.

Ezzel kész is van egy teljesen működőképes űrlapunk validációval PHP-ban és JavaScriptben is. A Nette validálási képességei sokkal szélesebbek, lehet feltételeket létrehozni, azok alapján megjeleníteni és elrejteni az oldal részeit stb. Mindent megtudhat az űrlap validációról szóló fejezetben.

Alapértelmezett értékek

Az űrlap elemeinek általában beállítunk alapértelmezett értékeket:

$form->addEmail('email', 'E-mail')
	->setDefaultValue($lastUsedEmail);

Gyakran hasznos az összes elem alapértelmezett értékét egyszerre beállítani. Például, ha az űrlap rekordok szerkesztésére szolgál. Kiolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Hívja meg a setDefaults()-t az elemek definiálása után.

Űrlap megjelenítése

Alapértelmezés szerint az űrlap táblázatként jelenik meg. Az egyes elemek megfelelnek az alapvető hozzáférhetőségi szabálynak – minden címke <label>-ként van megírva és összekapcsolva a megfelelő űrlap elemmel. A címkére kattintva a kurzor automatikusan az űrlap mezőbe kerül.

Minden elemhez beállíthatunk tetszőleges HTML attribútumokat. Például hozzáadhatunk egy placeholdert:

$form->addInteger('age', 'Életkor:')
	->setHtmlAttribute('placeholder', 'Kérjük, töltse ki az életkort');

Az űrlap megjelenítésének módjai valóban nagyon sokfélék, ezért ennek egy külön fejezetet szentelünk a megjelenítésről.

Leképezés osztályokra

Térjünk vissza a formSucceeded() metódushoz, amely a második paraméterben, a $data-ban kapja meg az elküldött adatokat ArrayHash objektumként. Mivel ez egy generikus osztály, valami olyasmi, mint a stdClass, hiányozni fog belőle bizonyos kényelem a munkavégzés során, mint például a property-k kódkiegészítése a szerkesztőkben vagy a statikus kódelemzés. Ezt meg lehetne oldani azzal, hogy minden űrlaphoz lenne egy konkrét osztályunk, amelynek property-jei az egyes elemeket reprezentálják. Pl.:

class RegistrationFormData
{
	public string $name;
	public ?int $age;
	public string $password;
}

Alternatívaként használhatja a konstruktort:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public int $age,
		public string $password,
	) {
	}
}

Az adatosztály property-jei lehetnek enumok is, és automatikusan leképeződnek.

Hogyan mondjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebben, mint gondolná. Elég csak az osztályt megadni a $data paraméter típusaként a kezelő metódusban:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data a RegistrationFormData példánya
	$name = $data->name;
	// ...
}

Típusként megadható az array is, és akkor az adatokat tömbként adja át.

Hasonló módon használható a getValues() függvény is, amelynek az osztály nevét vagy a hidratálandó objektumot paraméterként adjuk át:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Ha az űrlapok többszintű struktúrát alkotnak konténerekből, hozzon létre mindegyikhez külön osztályt:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public ?int $age;
	public string $password;
}

A leképezés ezután a $person property típusából felismeri, hogy a konténert a PersonFormData osztályra kell leképezni. Ha a property konténerek tömbjét tartalmazná, adja meg az array típust, és a leképezendő osztályt adja át közvetlenül a konténernek:

$person->setMappedType(PersonFormData::class);

Az űrlap adatosztályának tervét legeneráltathatja a Nette\Forms\Blueprint::dataClass($form) metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég kattintással kijelölni és bemásolni a projektbe.

Több gomb

Ha az űrlapnak több mint egy gombja van, általában meg kell különböztetnünk, hogy melyiket nyomták meg. Minden gombhoz létrehozhatunk saját kezelő függvényt. Ezt beállítjuk a esemény onClick kezelőjeként:

$form->addSubmit('save', 'Mentés')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Törlés')
	->onClick[] = [$this, 'deleteButtonPressed'];

Ezek a handlerek csak érvényesen kitöltött űrlap esetén hívódnak meg, ugyanúgy, mint az onSuccess esemény esetén. A különbség az, hogy első paraméterként az űrlap helyett átadható a küldő gomb, attól függően, hogy milyen típust ad meg:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Amikor az űrlapot az Enter gombbal küldik el, az úgy tekintendő, mintha az első gombbal küldték volna el.

onAnchor esemény

Amikor a factory metódusban (mint pl. a createComponentRegistrationForm) összeállítjuk az űrlapot, az még nem tudja, hogy el lett-e küldve, sem azt, hogy milyen adatokkal. Vannak azonban esetek, amikor szükségünk van az elküldött értékek ismeretére, például ezek alapján alakul az űrlap további formája, vagy szükségünk van rájuk a függő selectboxokhoz stb.

Az űrlapot összeállító kódrészletet ezért hagyhatjuk meghívni csak abban a pillanatban, amikor az ún. lehorgonyzott, tehát már kapcsolódik a presenterhez és ismeri az elküldött adatait. Ilyen kódot adunk át az $onAnchor tömbbe:

$country = $form->addSelect('country', 'Ország:', $this->model->getCountries());
$city = $form->addSelect('city', 'Város:');

$form->onAnchor[] = function () use ($country, $city) {
	// ez a függvény csak akkor hívódik meg, amikor az űrlap már tudja, hogy el lett-e küldve és milyen adatokkal
	// tehát használható a getValue() metódus
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Védelem a sebezhetőségekkel szemben

A Nette Framework nagy hangsúlyt fektet a biztonságra, ezért gondosan ügyel az űrlapok jó védelmére. Ezt teljesen átláthatóan teszi, és nem igényel manuális beállítást.

Amellett, hogy az űrlapokat megvédi a Cross Site Scripting (XSS) és a Cross-Site Request Forgery (CSRF) támadásoktól, számos apró biztonsági intézkedést tesz, amelyekre Önnek már nem kell gondolnia.

Például kiszűri a bemenetekből az összes vezérlőkaraktert és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlap adatai mindig tiszták lesznek. A select boxoknál és radio listáknál ellenőrzi, hogy a kiválasztott elemek valóban a kínáltak közül valók voltak-e, és nem történt-e hamisítás. Már említettük, hogy az egysoros szöveges bemeneteknél eltávolítja a sorvégi karaktereket, amelyeket egy támadó küldhetett. A többsoros bemeneteknél pedig normalizálja a sorvégi karaktereket. És így tovább.

A Nette megoldja Ön helyett azokat a biztonsági kockázatokat, amelyekről sok programozó nem is tudja, hogy léteznek.

Az említett CSRF támadás lényege, hogy a támadó ráveszi az áldozatot egy olyan oldalra, amely észrevétlenül végrehajt egy kérést az áldozat böngészőjében arra a szerverre, amelyen az áldozat be van jelentkezve, és a szerver azt hiszi, hogy a kérést az áldozat saját akaratából hajtotta végre. Ezért a Nette megakadályozza a POST űrlap elküldését más domainről. Ha valamilyen okból ki szeretné kapcsolni a védelmet, és engedélyezni szeretné az űrlap elküldését más domainről, használja a következőt:

$form->allowCrossOrigin(); // FIGYELEM! Kikapcsolja a védelmet!

Ez a védelem a _nss nevű SameSite cookie-t használja. A SameSite cookie segítségével történő védelem nem feltétlenül 100%-ban megbízható, ezért célszerű bekapcsolni a token alapú védelmet is:

$form->addProtection();

Javasoljuk, hogy így védje az adminisztrációs felületen lévő űrlapokat, amelyek érzékeny adatokat módosítanak az alkalmazásban. A keretrendszer a CSRF támadás ellen egy engedélyezési token generálásával és ellenőrzésével védekezik, amely a sessionben tárolódik. Ezért szükséges, hogy az űrlap megjelenítése előtt nyitva legyen a session. Az adminisztrációs felületen általában már el van indítva a session a felhasználó bejelentkezése miatt. Ellenkező esetben indítsa el a sessiont a Nette\Http\Session::start() metódussal.

Ugyanaz az űrlap több presenterben

Ha ugyanazt az űrlapot több presenterben is használni szeretné, javasoljuk, hogy hozzon létre hozzá egy factory-t, amelyet aztán átad a presenternek. Egy ilyen osztály megfelelő helye például az app/Forms könyvtár.

A factory osztály például így nézhet ki:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Név:');
		$form->addSubmit('send', 'Bejelentkezés');
		return $form;
	}
}

Az osztályt megkérjük az űrlap legyártására a presenter komponens factory metódusában:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// módosíthatjuk az űrlapot, itt például megváltoztatjuk a gomb címkéjét
	$form['send']->setCaption('Folytatás');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // és hozzáadunk egy handlert
	return $form;
}

Az űrlap feldolgozására szolgáló handler is származhat már a factory-ból:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Név:');
		$form->addSubmit('send', 'Bejelentkezés');
		$form->onSuccess[] = function (Form $form, $data): void {
			// itt végezzük el az űrlap feldolgozását
		};
		return $form;
	}
}

Nos, túl vagyunk a Nette űrlapok gyors bevezetésén. Próbáljon meg még belenézni a disztribúció examples könyvtárába, ahol további inspirációt találhat.

verzió: 4.0