Formuláře v presenterech
Nette Forms řádově usnadňují tvorbu a zpracování webových formulářů. V této kapitole se seznámíte s používáním formulářů uvnitř presenterů.
Pokud vás zajímá, jak je používat zcela samostatně bez zbytku frameworku, 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 HomePresenter 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('Home:');
}
}
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 (tj. je validní). 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. První parametr můžete vynechat, pokud objekt
formuláře nepotřebujete. A 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á.');
Kromě onSuccess
existuje ještě onSubmit
: callbacky se volají vždy po odeslání formuláře,
i tehdy, pokud není správně vyplněn. A dále onError
: callbacky se volají jen pokud odeslání validní není.
Zavolají se dokonce i tehdy, pokud v onSuccess
nebo onSubmit
znevalidníme formulář pomocí
addError()
.
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
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 šablony layoutu:
<script src="https://unpkg.com/nette-forms@3"></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;
}
Alternativně můžete využít konstruktor:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Property datové třídy mohou být také enumy a dojde k jejich automatickému namapování.
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.
Obdobným způsobem lze používat i funkci getValues()
, které název třídy nebo objekt k hydrataci předáme
jako parametr:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
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. 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 místo formulář může předat odesílací tlačítko,
záleží na typu, který uvedete:
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.
Událost onAnchor
Když v tovární metodě (jako je např. createComponentRegistrationForm
) sestavujeme formulář, ten ještě
neví, zda byl odeslán, ani s jakými daty. Jsou ale případy, kdy odeslané hodnoty potřebujeme znát, třeba se podle nich
odvíjí další podoba formuláře, nebo je potřebujeme pro závislé selectboxy atd.
Část kódu sestavující formulář proto můžete nechat zavolat až v okamžiku, když je tzv. ukotven, tedy je již
propojen s presenterem a zná svoje odeslaná data. Takový kód předáme do pole $onAnchor
:
$country = $form->addSelect('country', 'Stát:', $this->model->getCountries());
$city = $form->addSelect('city', 'Město:');
$form->onAnchor[] = function () use ($country, $city) {
// tato funkce se zavolá až bude formulář vědět, zda byl odeslán a s jakými daty
// lze tedy používat metodu getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
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->allowCrossOrigin(); // POZOR! Vypne ochranu!
Tato ochrana využívá SameSite cookie pojmenovanou _nss
. 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()
.
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.