Formuláře v presenterech

Nette Forms zásadně usnadňují vytváření a zpracování webových formulářů ve vašich aplikacích. V této kapitole se seznámíte s používáním formulářů uvnitř presenterů.

Pokud nepoužíváte presentery a Nette Application, je pro vás určen návod pro samostatné použití.

První formulář

Zkusíme si napsat jednoduchý registrační formulář. Jeho kód bude následující:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Jméno:');
$form->addPassword('password', 'Heslo:');
$form->addSubmit('send', 'Registrovat');
$form->onSuccess[] = [$this, 'formSucceeded'];

a v prohlížeči se zobrazí takto:

Formulář v presenteru je objekt třídy Nette\Application\UI\Form, její předchůdce Nette\Forms\Form je určen pro samostatné užití. Přidali jsem do něj tzv. prvky jméno, heslo a odesílací tlačítko. A nakonec řádek s $form->onSuccess říká, že po odeslání a úspěšné validaci se má zavolat metoda $this->formSucceeded().

Z pohledu presenteru je formulář běžná komponenta. Proto se s ním jako s komponentou zachází a začleníme ji do presenteru pomocí tovární metody. Bude to vypadat takto:

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

class HomepagePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Jméno:');
		$form->addPassword('password', 'Heslo:');
		$form->addSubmit('send', 'Registrovat');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// tady zpracujeme data odeslaná formulářem
		// $data->name obsahuje jméno
		// $data->password obsahuje heslo
		$this->flashMessage('Byl jste úspěšně registrován.');
		$this->redirect('Homepage:');
	}
}

A v šabloně formulář vykreslíme značkou {control}:

<h1>Registrace</h1>

{control registrationForm}

A to je vlastně vše :-) Máme funkční a perfektně zabezpečený formulář.

A teď si nejspíš říkáte, že to bylo moc hrr, přemýšlíte, jak je možné, že se zavolá metoda formSucceeded() a co jsou parametry, které dostává. Jistě, máte pravdu, tohle si zaslouží vysvětlení.

Nette totiž přichází se svěžím mechanismem, kterému říkáme Hollywood style. Místo toho, abyste se jako vývojář musel neustále vyptávat, jestli se něco událo („byl formulář odeslaný?“, „byl odeslaný validně?“ a „nedošlo k jeho podvržení?“), řeknete frameworku „až bude formulář validně vyplněný, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce, které se volají, až nastane určitá událost. A jazyk jim předává příslušné argumenty.

Právě takhle je postaven i výše uvedený kód presenteru. Pole $form->onSuccess představuje seznam PHP callbacků, které Nette zavolá v okamžiku, kdy je formulář odeslán a správně vyplněn. V rámci životního cyklu presenteru jde o tzv. signál, volají se tedy po action* metodě a před render* metodou. A každému callbacku předá jako první parametr samotný formulář a jako druhý odeslaná data v podobě objektu ArrayHash. Druhý parametr umí být mazanější, ale o tom až 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, 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.

Zkuste si přidat i další formulářové prvky.

Přístup k prvkům

Formulář je komponenta presenteru, v našem případě pojmenovaná registrationForm (dle jména tovární metody createComponentRegistrationForm), takže kdekoliv v presenteru se k formuláři dostanete pomocí:

$form = $this->getComponent('registrationForm');
// alternativní syntax: $form = $this['registrationForm'];

Komponenty jsou i jednotlivé prvky formuláře, proto se k nim dostanete stejným způsobem:

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

Prvky se odstraní pomocí unset:

unset($form['name']);

Validační pravidla

Několikrát tu padlo 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. Pokud vycházíte z nette/sandbox, už jej máte zalinkovaný v layoutu stránky. V opačném případě si jej vložte do stránky:

<script src="https://nette.github.io/resources/js/3/netteForms.min.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 k metodě formSucceeded(), která ve druhém parametru $data dostává 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;
}

Jak říci Nette, aby nám data vracel jako objekty této třídy? Snadněji než si myslíte. Stačí pouze třídu uvést jako typ parametru $data v obslužné metodě:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name je instance RegistrationFormData
	$name = $data->name;
	...

Jako typ lze uvést také array a pak data předá jako pole.

Více tlačítek

Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozlišit, které z nich bylo stlačeno. Můžeme si pro každé tlačítko vytvořit vlastní obslužnou funkci. Nastavíme ji jako handler pro událost onClick:

$form->addSubmit('save', 'Uložit')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Smazat')
	->onClick[] = [$this, 'deleteButtonPressed'];

Tyto handlery se volají pouze v případě validně vyplněného formuláře, stejně jako v případě události onSuccess. Rozdíl je v tom, že jako první parametr se nepředává formulář, ale odesílací tlačítko:

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

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ářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat.

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. Pokud z nějakého důvodu chcete ochranu vypnout a dovolit odesílat formulář z jiné domény, použijte:

$form->disableSameSiteProtection(); // POZOR! Vypne ochranu!

Stejný formulář ve více presenterech

Pokud potřebujete jeden formulář použít ve více presenterech, doporučujeme si pro něj vytvořit továrnu, kterou si pak předáte do presenteru. Vhodné umístění pro takovou třídu je např. adresář app/Forms.

Tovární třída může vypadat třeba takto:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Jméno:');
		$form->addSubmit('send', 'Přihlásit se');
		return $form;
	}
}

Třídu požádáme o vyrobení formuláře v tovární metodě na komponenty v presenteru:

public function __construct(SignInFormFactory $formFactory)
{
	$this->formFactory = $formFactory;
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// můžeme formulář pozměnit, zde například měníme popisku na tlačítku
	$form['send']->setCaption('Pokračovat');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // a přidáme handler
	return $form;
}

Handler pro zpracování formuláře může být také dodán už z továrny:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Jméno:');
		$form->addSubmit('send', 'Přihlásit se');
		$form->onSuccess[] = function (Form $form, $data): void {
			// zde provedeme zpracování formuláře
		};
		return $form;
	}
}

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.


Související články na blogu

Vylepšit tuto stránku