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.

verze: 4.0 3.x 2.x