Formanyomtatványok az előadóknál
A Nette Forms jelentősen megkönnyíti a webes űrlapok létrehozását és feldolgozását. Ebben a fejezetben megtanulja, hogyan használhatja az űrlapokat a prezentereken belül.
Ha teljesen önállóan, a keretrendszer többi része nélkül szeretné használni őket, akkor van egy útmutató az önálló űrlapokhoz.
Első űrlap
Megpróbálunk írni egy egyszerű regisztrációs űrlapot. A kódja így fog kinézni:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];
A böngészőben az eredménynek így kell kinéznie:
A prezenterben lévő űrlap a Nette\Application\UI\Form
osztály objektuma, elődje a
Nette\Forms\Form
önálló használatra készült. Hozzáadtuk a név, a jelszó és a küldés gomb mezőit. Végül
a $form->onSuccess
sorban az áll, hogy a beküldés és a sikeres érvényesítés után a
$this->formSucceeded()
metódust kell meghívni.
A bemutató szempontjából az űrlap egy közös komponens. Ezért komponensként kezeljük, és a factory metódus segítségével beépítjük a prezentálóba. 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álj');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// itt fogjuk feldolgozni az űrlap által küldött adatokat.
// $data->name tartalmazza a nevet
// $data->password tartalmazza a jelszót
$this->flashMessage('Sikeresen regisztrált.');
$this->redirect('Home:');
}
}
A sablonban történő megjelenítés pedig a {control}
tag használatával történik:
<h1>Registration</h1>
{control registrationForm}
És ez minden :-) Van egy működőképes és tökéletesen biztonságos űrlapunk.
Most valószínűleg azt gondolod, hogy ez túl gyors volt, és azon tűnődsz, hogy hogyan lehetséges, hogy a
formSucceeded()
metódus meghívásra kerül, és milyen paramétereket kap. Persze, igazad van, ez megérdemel egy
magyarázatot.
Nette klassz mechanizmussal állt elő, amit hollywoodi stílusnak nevezünk. Ahelyett, hogy állandóan kérdezgetni kellene, hogy történt-e valami (“elküldték-e az űrlapot?”, “érvényesen elküldték-e?” vagy “nem hamisították-e?”), azt mondod a keretrendszernek, hogy “ha az űrlap érvényesen kitöltött, hívd meg ezt a metódust”, és hagyd rajta a további munkát. Ha JavaScriptben programozol, akkor 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 nyelv pedig átadja nekik a megfelelő argumentumokat.
Így épül fel a fenti prezenter kódja. A $form->onSuccess
array a PHP visszahívások listáját jelenti,
amelyeket a Nette akkor hív meg, ha az űrlapot elküldték és helyesen kitöltötték. A prezenter életciklusán belül ez egy
úgynevezett szignál, tehát a action*
metódus után és a render*
metódus előtt hívódnak meg. És
minden callbacknek átadja magát az űrlapot az első paraméterben, és a beküldött adatokat ArrayHash objektumként a másodikban. Az első paramétert
elhagyhatjuk, ha nincs szükségünk a form objektumra. A második paraméter még hasznosabb lehet, de erről később.
A $data
objektum tartalmazza a name
és password
tulajdonságokat a felhasználó által
megadott adatokkal. Általában az adatokat közvetlenül elküldjük további feldolgozásra, ami lehet például az adatbázisba
való beillesztés. A feldolgozás során azonban előfordulhat hiba, például a felhasználónév már foglalt. Ebben az
esetben a addError()
segítségével visszaadjuk a hibát az űrlapnak, és hagyjuk, hogy újra kirajzolódjon,
hibaüzenettel:
$form->addError('Sorry, username is already in use.');
A onSuccess
mellett létezik a onSubmit
is : a visszahívások mindig az űrlap elküldése után
hívódnak meg, még akkor is, ha az űrlap nem lett helyesen kitöltve. És végül a onError
: a callbackek csak
akkor hívódnak meg, ha a beküldés nem érvényes. Ezek még akkor is meghívódnak, ha a onSuccess
vagy a
onSubmit
segítségével érvénytelenítjük az űrlapot a addError()
segítségével.
Az űrlap feldolgozása után átirányítjuk a következő oldalra. Ez megakadályozza, hogy az űrlapot véletlenül újra beküldjék a frissítés, vissza gombra kattintva, vagy a böngésző előzményeit mozgatva.
Próbáljon meg több űrlapvezérlőt hozzáadni.
Hozzáférés a vezérlőkhöz
Az űrlap a prezenter egyik komponense, esetünkben a registrationForm
névvel (a
createComponentRegistrationForm
gyári metódus neve után), így a prezenterben bárhol elérhetjük az űrlapot a
következővel:
$form = $this->getComponent('registrationForm');
// alternatív szintaxis: $form = $this['registrationForm'];
Az egyes űrlapvezérlők is komponensek, így ugyanígy elérhetjük őket:
$input = $form->getComponent('name'); // vagy $input = $form['name'];
$button = $form->getComponent('send'); // vagy $button = $form['send'];
A vezérlőelemek eltávolítása az unset használatával történik:
unset($form['name']);
Érvényesítési szabályok
Az érvényes szót többször használták, de az űrlapnak még nincsenek érvényesítési szabályai. Javítsuk ki.
A név kötelező lesz, ezért jelöljük meg a setRequired()
metódussal, amelynek argumentuma a hibaüzenet
szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki. Ha nem adunk meg argumentumot, akkor az alapértelmezett
hibaüzenetet használjuk.
$form->addText('name', 'Name:')
->setRequired('Please fill your name.');
Próbáljuk meg elküldeni az űrlapot a név kitöltése nélkül, és látni fogjuk, hogy hibaüzenet jelenik meg, és a böngésző vagy a szerver elutasítja a kitöltésig.
Ugyanakkor nem fogja tudni becsapni a rendszert azzal, hogy például csak szóközöket ír be a beviteli mezőbe. Szó sem lehet róla. A Nette automatikusan levágja a bal és jobb oldali szóközöket. Próbálja ki! Ezt mindig meg kellene tennie minden egysoros bevitelnél, de gyakran elfelejtik. A Nette automatikusan elvégzi. (Megpróbálhatja becsapni az űrlapokat, és többsoros karakterláncot küldhet névként. A Nette még ebben az esetben sem fog becsapni, és a sortörések szóközökre változnak).
Az űrlap mindig a szerveroldalon validálódik, de a JavaScript validáció is generálódik, ami gyors, és a felhasználó
azonnal értesül a hibáról, anélkül, hogy az űrlapot a szerverre kellene küldeni. Ezt a netteForms.js
szkript
kezeli. Ezt illessze be az elrendezési 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 szükséges mezőket a
required
CSS osztályú elemekbe illeszti be. Próbálja meg a következő stílust hozzáadni a sablonhoz, és a
“Név” felirat piros lesz. Elegánsan jelöljük a kötelező mezőket a felhasználók számára:
<style>
.required label { color: maroon }
</style>
További érvényesítési szabályokat a addRule()
módszerrel adunk hozzá. Az első paraméter a szabály, a
második ismét a hibaüzenet szövege, és az opcionális érvényesítési szabály argumentum következhet. Mit jelent ez?
Az űrlap kap egy másik opcionális bemenetet életkor azzal a feltétellel, hogy annak számnak kell lennie
(addInteger()
) és bizonyos határok között ($form::Range
). És itt fogjuk használni a
addRule()
harmadik argumentumát , magát a tartományt:
$form->addInteger('age', 'Age:')
->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);
Ha a felhasználó nem tölti ki a mezőt, az érvényesítési szabályok nem lesznek ellenőrizve, mivel a mező opcionális.
Nyilvánvalóan van hely 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ű űrlapot
hoznánk létre, és a számokat tartalmazó üzenetet több nyelvre kellene lefordítani, ez megnehezítené az értékek
módosítását. Emiatt a %d
helyettesítő karakterek használhatók:
->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);
Térjünk vissza a jelszó mezőhöz, tegyük követelményessé, és ellenőrizzük a minimális
jelszóhosszúságot ($form::MinLength
), ismét az üzenetben szereplő helyettesítő karakterek
segítségével:
$form->addPassword('password', 'Password:')
->setRequired('Pick a password')
->addRule($form::MinLength, 'Your password has to be at least %d long', 8);
Adjunk hozzá egy passwordVerify
mezőt az űrlaphoz, ahol a felhasználó újra beírja a jelszót, az
ellenőrzéshez. Érvényesítési szabályok segítségével ellenőrizzük, hogy mindkét jelszó azonos-e
($form::Equal
). Érvként pedig szögletes zárójelek segítségével megadjuk
az első jelszóra való hivatkozást:
$form->addPassword('passwordVerify', 'Password again:')
->setRequired('Fill your password again to check for typo')
->addRule($form::Equal, 'Password mismatch', $form['password'])
->setOmitted();
A setOmitted()
segítségével egy olyan elemet jelöltünk meg, amelynek értéke nem igazán érdekel minket,
és amely csak az érvényesítés miatt létezik. Az értékét nem adjuk át a $data
.
Egy teljesen működőképes űrlapunk van, PHP és JavaScript validációval. A Nette validálási lehetőségei sokkal szélesebb körűek, létrehozhatunk feltételeket, megjeleníthetjük és elrejthetjük az oldal egyes részeit ezek alapján, stb. Mindent megtudhat az űrlapok validálásáról szóló fejezetben.
Alapértelmezett értékek
Gyakran állítunk be alapértelmezett értékeket űrlapvezérlőkhöz:
$form->addEmail('email', 'Email')
->setDefaultValue($lastUsedEmail);
Gyakran hasznos, ha egyszerre állítjuk be az összes vezérlőelem alapértelmezett értékeit. Például amikor az űrlapot rekordok szerkesztésére használjuk. Beolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
A vezérlőelemek definiálása után hívjuk meg a setDefaults()
címet.
Az űrlap megjelenítése
Az űrlap alapértelmezés szerint táblázatként jelenik meg. Az egyes vezérlőelemek az alapvető webes
hozzáférhetőségi irányelveket követik. Minden címke <label>
elemként jön létre, és a bemenetükhöz
kapcsolódik, a címkére kattintva a kurzor a bemenetre kerül.
Az egyes elemekhez bármilyen HTML-attribútumot beállíthatunk. Például adjunk hozzá egy helyőrzőt:
$form->addInteger('age', 'Age:')
->setHtmlAttribute('placeholder', 'Please fill in the age');
Valóban sokféleképpen lehet megjeleníteni egy űrlapot, ezért ez egy külön fejezet a megjelenítésről.
Osztályok leképezése
Térjünk vissza a formSucceeded()
metódushoz, amelynek második paraméterében a $data
a
ArrayHash
objektumként kapja meg az elküldött adatokat. Mivel ez egy általános osztály, valami olyasmi, mint a
stdClass
, hiányozni fog néhány kényelmi funkció a vele való munka során, például a tulajdonságok
kódkiegészítése a szerkesztőkben vagy a statikus kódelemzésben. Ezt úgy lehetne megoldani, hogy minden egyes űrlaphoz
külön osztályunk van, amelynek tulajdonságai az egyes vezérlőelemeket képviselik. Pl:
class RegistrationFormData
{
public string $name;
public int $age;
public string $password;
}
Alternatívaként használhatja a konstruktort is:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Az adatosztály tulajdonságai lehetnek enumok is, és automatikusan le lesznek képezve.
Hogyan mondhatjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebb, mint
gondolnád. Mindössze annyit kell tennie, hogy a kezelőben a $data
paraméter típusaként megadja az
osztályt:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name is instance of RegistrationFormData
$name = $data->name;
// ...
}
Megadhatja a array
típust is, és akkor az adatokat tömbként adja át.
Hasonló módon használhatjuk a getValues()
metódust is, amelynek paramétereként átadjuk az osztály nevét
vagy a hidratálandó objektumot:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Ha az űrlapok konténerekből álló többszintű struktúrából állnak, hozzunk 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 ekkor a $person
tulajdonságtípusból tudja, hogy a konténert a PersonFormData
osztályhoz kell leképeznie. Ha a tulajdonság tárolók tömbjét tartalmazná, adja meg a array
típust, és adja
át a leképezendő osztályt közvetlenül a tárolóhoz:
$person->setMappedType(PersonFormData::class);
Egy űrlap adatosztályának javaslatát a Nette\Forms\Blueprint::dataClass($form)
metódussal generálhatja, amely a böngészőoldalra nyomtatja ki. Ezután egyszerűen rákattintva kiválaszthatja és
bemásolhatja a kódot a projektjébe.
Többszörös beküldőgombok
Ha az űrlapon egynél több gomb van, általában meg kell különböztetnünk, hogy melyiket nyomta meg. Minden egyes gombhoz
létrehozhatunk saját függvényt. Állítsuk be kezelőként a onClick
eseményhez:
$form->addSubmit('save', 'Save')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Delete')
->onClick[] = [$this, 'deleteButtonPressed'];
Ezek a kezelők is csak abban az esetben hívódnak meg, ha az űrlap érvényes, mint a onSuccess
esemény
esetében. A különbség az, hogy az első paraméter a megadott típustól függően az űrlap helyett a submit gomb
objektuma lehet:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Ha egy űrlapot az Enter billentyűvel küldünk el, a rendszer úgy kezeli, mintha az első gombbal küldtük volna el.
Esemény onAnchor
Amikor egy űrlapot egy gyári metódusban (például a createComponentRegistrationForm
) építünk fel, még nem
tudja, hogy elküldték-e, vagy hogy milyen adatokkal küldték el. Vannak azonban olyan esetek, amikor tudnunk kell a beküldött
értékeket, esetleg tőlük függ, hogy milyen lesz az űrlap kinézete, vagy függő selectboxokhoz használjuk őket, stb.
Ezért lehet, hogy az űrlapot felépítő kódot akkor hívjuk meg, amikor lehorgonyzott, azaz már kapcsolódik a
prezenterhez, és ismeri a beküldött adatokat. Az ilyen kódot a $onAnchor
tömbbe fogjuk elhelyezni:
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');
$form->onAnchor[] = function () use ($country, $city) {
// ez a függvény akkor hívódik meg, ha az űrlap ismeri a beküldött adatokat.
// így használhatja a getValue() metódust.
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Sebezhetőségi védelem
A Nette Framework nagy erőfeszítéseket tesz a biztonság érdekében, és mivel az űrlapok a leggyakoribb felhasználói bevitel, a Nette űrlapok szinte áthatolhatatlanok. Mindent dinamikusan és átláthatóan tart fenn, semmit sem kell manuálisan beállítani.
Amellett, hogy megvédi az űrlapokat az olyan jól ismert sebezhetőségekre irányuló támadások ellen, mint a Cross-Site Scripting (XSS ) és a Cross-Site Request Forgery (CSRF), rengeteg apró biztonsági feladatot is elvégez, amelyekre már nem kell gondolnia.
Például kiszűri az összes vezérlő karaktert a bemenetekből, és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlapból származó adatok mindig tiszták lesznek. A kiválasztó dobozok és rádiólisták esetében ellenőrzi, hogy a kiválasztott elemek valóban a felkínáltak közül kerültek-e ki, és nem történt-e hamisítás. Már említettük, hogy az egysoros szövegbevitel esetében eltávolítja a sor végi karaktereket, amelyeket egy támadó oda küldhet. Többsoros bevitel esetén normalizálja a sor végi karaktereket. És így tovább.
A Nette olyan biztonsági réseket is kijavít az Ön számára, amelyekről a legtöbb programozónak fogalma sincs, hogy léteznek.
Az említett CSRF-támadás lényege, hogy a támadó egy olyan oldal meglátogatására csábítja az áldozatot, amely némán végrehajt egy kérést az áldozat böngészőjében a szerver felé, ahol az áldozat éppen bejelentkezve van, és a szerver azt hiszi, hogy a kérést az áldozat akaratából tette. Ezért a Nette megakadályozza, hogy az űrlapot egy másik tartományból POST-on keresztül küldjék el. Ha valamilyen oknál fogva ki akarja kapcsolni a védelmet, és engedélyezni szeretné, hogy az űrlapot egy másik tartományból küldjék el, 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-védelem nem biztos, hogy 100%-ig
megbízható, ezért érdemes bekapcsolni a token-védelmet:
$form->addProtection();
Erősen ajánlott ezt a védelmet alkalmazni az alkalmazás adminisztrációs részében lévő, érzékeny adatokat
módosító űrlapokra. A keretrendszer a CSRF-támadás ellen a munkamenetben tárolt hitelesítési token generálásával és
érvényesítésével védekezik (az érv a token lejárta esetén megjelenő hibaüzenet). Ezért szükséges, hogy az űrlap
megjelenítése előtt elinduljon egy munkamenet. A webhely adminisztrációs részében a munkamenet általában már elindult,
a felhasználó bejelentkezése miatt. Ellenkező esetben a munkamenetet a Nette\Http\Session::start()
metódussal
kell elindítani.
Egy űrlap használata több bemutatóban
Ha egy űrlapot több prezenterben kell használnia, javasoljuk, hogy hozzon létre egy gyárat, amelyet aztán átad a
prezenternek. Egy ilyen osztály megfelelő helye például a app/Forms
könyvtár.
A gyári osztály így nézhet ki:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Log in');
return $form;
}
}
A gyári metódusban az osztálytól azt kérjük, hogy állítsa elő az űrlapot a prezenterben lévő komponensek számára:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// megváltoztathatjuk az űrlapot, itt például a gomb címkéjét változtatjuk meg.
$form['login']->setCaption('Continue');
$form->onSuccess[] = [$this, 'signInFormSubmitted']; // és hozzáadjuk a kezelőt.
return $form;
}
Az űrlapfeldolgozás kezelője is a gyárból szállítható:
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 feldolgozzuk a beküldött űrlapunkat
};
return $form;
}
}
Tehát, van egy gyors bevezetés az űrlapok Nette-ben. További inspirációért próbáljon meg szétnézni a disztribúcióban található példák könyvtárában.