Formuláře použité samostatně
Nette Forms řádově usnadňují tvorbu a zpracování webových formulářů. Můžete je používat ve vašich aplikacích zcela samostatně bez zbytku frameworku, což si ukážeme v této kapitole.
Pokud ale používáte Nette Application a presentery, je pro vás určen návod pro použití v presenterech.
První formulář
Zkusíme si napsat jednoduchý registrační formulář. Jeho kód bude následující (celý kód):
use Nette\Forms\Form;
$form = new Form;
$form->addText('name', 'Jméno:');
$form->addPassword('password', 'Heslo:');
$form->addSubmit('send', 'Registrovat');
Velmi snadno ho vykreslíme:
$form->render();
a v prohlížeči se zobrazí takto:
Formulář je objekt třídy Nette\Forms\Form
(třída Nette\Application\UI\Form
se používá
v presenterech). Přidali jsem do něj tzv. prvky jméno, heslo a odesílací tlačítko.
A teď formulář oživíme. Dotazem na $form->isSuccess()
zjistíme, zda byl formulář odeslán a zda byl
vyplněn validně. Pokud ano, data vypíšeme. Za definici formuláře tedy doplníme:
if ($form->isSuccess()) {
echo 'Formulář byl správně vyplněn a odeslán';
$data = $form->getValues();
// $data->name obsahuje jméno
// $data->password obsahuje heslo
var_dump($data);
}
Metoda getValues()
vrací odeslaná data v podobě objektu ArrayHash. Jak to změnit si ukážeme později. Objekt $data
obsahuje klíče name
a password
s údaji, které vyplnil uživatel.
Obvykle data rovnou posíláme k dalšímu zpracování, což může být například vložení do databáze. Během
zpracování se ale může objevit chyba, například uživatelské jméno už je obsazené. V takovém případě chybu
předáme zpět do formuláře pomocí addError()
a necháme jej vykreslit znovu, i s chybovou hláškou.
$form->addError('Omlouváme se, toto uživatelské jméno už někdo používá.');
Po zpracování formuláře přesměrujeme na další stránku. Zabrání se tak nechtěnému opětovnému odeslání formuláře tlačítkem obnovit, zpět nebo pohybem v historii prohlížeče.
Formulář se standardně odesílá metodou POST a to na stejnou stránku. Obojí se dá změnit:
$form->setAction('/submit.php');
$form->setMethod('GET');
A to je vlastně vše :-) Máme funkční a perfektně zabezpečený formulář.
Zkuste si přidat i další formulářové prvky.
Přístup k prvkům
Formulář i jednotlivé jeho prvky nazýváme komponentami. Tvoří komponentový strom, kde kořenem je právě formulář. K jednotlivým prvkům formuláře se dostaneme tímto způsobem:
$input = $form->getComponent('name');
// alternativní syntax: $input = $form['name'];
$button = $form->getComponent('send');
// alternativní syntax: $button = $form['send'];
Prvky se odstraní pomocí unset:
unset($form['name']);
Validační pravidla
Padlo tu slovo validní, ale formulář zatím žádná validační pravidla nemá. Pojďme to napravit.
Jméno bude povinné, proto je označíme metodou setRequired()
, jejíž argument je text chybové hlášky,
která se zobrazí, pokud uživatel jméno nevyplní. Pokud argument neuvedeme, použije se výchozí chybová hláška.
$form->addText('name', 'Jméno:')
->setRequired('Zadejte prosím jméno');
Zkuste si odeslat formulář bez vyplněného jména a uvidíte, že se zobrazí chybová hláška a prohlížeč či server jej bude odmítat do té doby, dokud políčko nevyplníte.
Zároveň systém neošidíte tím, že do políčka napíšete třeba jen mezery. Kdepak. Nette levo- i pravostranné mezery automaticky odstraňuje. Vyzkoušejte si to. Je to věc, kterou byste měli s každým jednořádkovým inputem vždy udělat, ale často se na to zapomíná. Nette to dělá automaticky. (Můžete zkusit ošálit formulář a jako jméno poslat víceřádkový řetězec. Ani tady se Nette nenechá zmást a odřádkování změní na mezery.)
Formulář se vždy validuje na straně serveru, ale také se generuje JavaScriptová validace, která proběhne bleskově a
uživatel se o chybě dozví okamžitě, bez nutnosti formulář odesílat na server. Tohle má na starosti skript
netteForms.js
. Vložte jej do stránky:
<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></script>
Pokud se podíváte do zdrojového kódu stránky s formulářem, můžete si všimnout, že Nette povinné prvky vkládá do
elementů s CSS třídou required
. Zkuste přidat do šablony následující stylopis a popiska „Jméno“ bude
červená. Elegantně tak uživatelům vyznačíme povinné prvky:
<style>
.required label { color: maroon }
</style>
Další validační pravidla přidáme metodou addRule()
. První parametr je pravidlo, druhý je opět text
chybové hlášky a může ještě následovat argument validačního pravidla. Co se tím myslí?
Formulář rozšíříme o nové nepovinné políčko „věk“, které musí být celé číslo (addInteger()
)
a navíc v povoleném rozsahu ($form::RANGE
). A zde právě využijeme třetí parametr metody
addRule()
, kterým předáme validátoru požadovaný rozsah jako dvojici [od, do]
:
$form->addInteger('age', 'Věk:')
->addRule($form::RANGE, 'Věk musí být od 18 do 120', [18, 120]);
Pokud uživatel políčko nevyplní, nebudou se validační pravidla ověřovat, neboť prvek je nepovinný.
Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně,
což není ideální. Pokud bychom tvořili vícejazyčné formuláře a hláška obsahující čísla by byla přeložena do
více jazyků, ztížila by se případná změna hodnot. Z toho důvodu je možné použít zástupné znaky %d
a
Nette hodnoty doplní:
->addRule($form::RANGE, 'Věk musí být od %d do %d let', [18, 120]);
Vraťme se k prvku password
, který taktéž učiníme povinným a ještě ověříme minimální délku hesla
($form::MIN_LENGTH
), opět s využitím zástupného znaku:
$form->addPassword('password', 'Heslo:')
->setRequired('Zvolte si heslo')
->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8);
Přidáme do formuláře ještě políčko passwordVerify
, kde uživatel zadá heslo ještě jednou, pro kontrolu.
Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná ($form::EQUAL
). A jako parametr dáme
odvolávku na první heslo pomocí hranatých závorek:
$form->addPassword('passwordVerify', 'Heslo pro kontrolu:')
->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu')
->addRule($form::EQUAL, 'Hesla se neshodují', $form['password'])
->setOmitted();
Pomocí setOmitted()
jsme označili prvek, na jehož hodnotě nám vlastně nezáleží a která existuje jen
z důvodu validace. Hodnota se nepředá do $data
.
Tímto máme hotový plně funkční formulář s validací v PHP i JavaScriptu. Validační schopnosti Nette jsou daleko širší, dají se vytvářet podmínky, nechávat podle nich zobrazovat a skrývat části stránky atd. Vše se dozvíte v kapitole o validaci formulářů.
Výchozí hodnoty
Prvkům formuláře běžne nastavujeme výchozí hodnoty:
$form->addEmail('email', 'E-mail')
->setDefaultValue($lastUsedEmail);
Často se hodí nastavit výchozí hodnoty všem prvkům současně. Třeba když formulář slouží k editaci záznamů. Přečteme záznam z databáze a nastavíme výchozí hodnoty:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Volejte setDefaults()
až po definici prvků.
Vykreslení formuláře
Standardně se formulář vykreslí jako tabulka. Jednotlivé prvky splňují základní pravidlo přístupnosti – všechny
popisky jsou zapsány jako <label>
a provázané s příslušným formulářovým prvkem. Při kliknutí na
popisku se kurzor automaticky objeví ve formulářovém políčku.
Každému prvku můžeme nastavovat libovolné HTML atributy. Třeba přidat placeholder:
$form->addInteger('age', 'Věk:')
->setHtmlAttribute('placeholder', 'Prosím vyplňte věk');
Způsobů, jak vykreslit formulář, je opravdu velké množství, takže je tomu věnována samostatná kapitola o vykreslování.
Mapování na třídy
Vraťme se ke zpracování formulářových dat. Metoda getValues()
nám vracela odeslaná data jako objekt
ArrayHash
. Protože jde o generickou třídu, něco jako stdClass
, bude nám při práci s ní chybět
určitý komfort, jako je třeba našeptávání properties v editorech nebo statická analýza kódu. To by se dalo vyrešit
tím, že bychom pro každý formulář měli konkrétní třídu, jejíž properties reprezentují jednotlivé
prvky. Např.:
class RegistrationFormData
{
/** @var string */
public $name;
/** @var int */
public $age;
/** @var string */
public $password;
}
Od PHP 8.0 a formulářů v3.1 můžete použít tento elegantní zápis, který využívá konstruktor:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze název třídy nebo objekt k hydrataci uvést jako parametr:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Jako parametr lze uvést také 'array'
a pak data vrátí jako pole.
Pokud formuláře tvoří víceúrovňovou strukturu složenou z kontejnerů, vytvořte pro každý samostatnou třídu:
$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;
}
Mapování pak z typu property $person
pozná, že má kontejner mapovat na třídu PersonFormData
.
Pokud by property obsahovala pole kontejnerů, uveďte typ array
a třídu pro mapování předejte přímo
kontejneru:
$person->setMappedType(PersonFormData::class);
Více tlačítek
Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozlišit, které z nich bylo stlačeno. Tuto
informaci nám vrátí metoda isSubmittedBy()
tlačítka:
$form->addSubmit('save', 'Uložit');
$form->addSubmit('delete', 'Smazat');
if ($form->isSuccess()) {
if ($form['save']->isSubmittedBy()) {
// ...
}
if ($form['delete']->isSubmittedBy()) {
// ...
}
}
Dotaz na $form->isSuccess()
nevynechejte, ověříte tím validitu dat.
Když se formulář odešle tlačítkem Enter, bere se to jako kdyby byl odeslán prvním tlačítkem.
Ochrana před zranitelnostmi
Nette Framework klade velký důraz na bezpečnost a proto úzkostlivě dbá na dobré zabezpečení formulářů.
Kromě toho, že formuláře ochrání před útokem Cross Site Scripting (XSS) a Cross-Site Request Forgery (CSRF), dělá spoustu drobných zabezpečení, na které vy už nemusíte myslet.
Tak třeba odfiltruje ze vstupů všechny kontrolní znaky a prověří validitu UTF-8 kódování, takže data z formuláře budou vždycky čistá. U select boxů a radio listů ověřuje, že vybrané položky byly skutečně z nabízených a nedošlo k podvrhu. Už jsme zmiňovali, že u jednořádkových textových vstupů ostraňuje znaky konce řádků, které tam mohl poslat útočník. U víceřádkových vstupů zase normalizuje znaky pro konce řádků. A tak dále.
Nette za vás řeší bezpečnostní rizika, o kterých spousta programátorů ani netuší, že existují.
Zmíněný CSRF útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně v prohlížeči oběti vykoná požadavek na server, na kterém je oběť přihlášena, a server se domnívá, že požadavek vykonala oběť o své vůli. Proto Nette zabraňuje odeslání POST formuláře z jiné domény (od verze 3.1). Pokud z nějakého důvodu chcete ochranu vypnout a dovolit odesílat formulář z jiné domény, použijte:
$form->allowCrossOrigin(); // POZOR! Vypne ochranu!
Tato ochrana využívá SameSite cookie pojmenovanou _nss
. Vytvářejte proto objekt formuláře ještě před
odesláním prvního výstupu, aby bylo možné cookie odeslat.
Ochrana pomocí SameSite cookie nemusí být 100% spolehlivá, proto je vhodné zapnout ještě ochranu pomocí tokenu:
$form->addProtection();
Doporučujeme takto chránit formuláře v administrační části webu, které mění citlivá data v aplikaci. Framework se
proti útoku CSRF brání vygenerováním a ověřováním autorizačního tokenu, který se ukládá do session. Proto je nutné
před zobrazením formuláře mít otevřenou session. V administrační části webu obvykle už session nastartovaná je kvůli
přihlášení uživatele. Jinak session nastartujte metodou Nette\Http\Session::start()
.
Tak, máme za sebou rychlý úvod do formulářů v Nette. Zkuste se ještě podívat do adresáře examples v distrubuci, kde najdete další inspiraci.