Formuláře

Nette Forms výrazně usnadňují vytváření a zpracování webových formulářů ve vašich aplikacích. Co všechno umí?

  • validovat odeslaná data na straně serveru i JavaScriptem
  • poskytují zabezpečení proti zranitelnostem
  • zvládají několik režimů vykreslování
  • vícejazyčnost

Nette Framework klade velký důraz na bezpečnost aplikací, a proto úzkostlivě dbá i na dobré zabezpečení formulářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat. Ochrání vaše aplikace před útokem Cross Site Scripting (XSS)Cross-Site Request Forgery (CSRF), odfiltruje ze vstupů kontrolní znaky, ověří validitu UTF-8 kódování nebo jestli nejsou položky vybrané v select boxech podvržené atd.

Použitím Nette Forms se vyhneme celé řadě rutinních úkolů, jako je třeba psaní dvojí validace (na straně serveru a klienta), minimalizujeme pravděpodobnost vzniku chyb a bezpečnostních děr.

První formulář

Vytvoříme si v naší aplikaci jednoduchý registrační formulář. Přidáme ho do presenteru pomocí tzv. továrny:

use Nette\Application\UI;

class HomepagePresenter extends UI\Presenter
{

    // ...

    protected function createComponentRegistrationForm()
    {
        $form = new UI\Form;
        $form->addText('name', 'Jméno:');
        $form->addPassword('password', 'Heslo:');
        $form->addSubmit('login', 'Registrovat');
        $form->onSuccess[] = [$this, 'registrationFormSucceeded'];
        return $form;
    }

    // volá se po úspěšném odeslání formuláře
    public function registrationFormSucceeded(UI\Form $form, $values)
    {
        // ...
        $this->flashMessage('Byl jste úspěšně registrován.');
        $this->redirect('Homepage:');
    }
}

v šabloně ho vykreslíme makrem control:

{control registrationForm}

a v prohlížeči bude vypadat takto:

Vytvořili jsme formulář s prvky jméno a heslo, který po odeslání a úspěšné validaci zavolá metodu registrationFormSucceeded(). Jako první parametr této metody je předán samotný formulář. Do druhého parametru se předávají odeslané hodnoty formuláře v objektu ArrayHash. Pokud chceme obdržet místo objektu pole, dáme parametru typehint array $values. Pro získání odeslaných hodnot můžeme také použít funkci $values = $form->getValues($asArray = false).

V rámci životního cyklu presenteru dochází ke zpracování formuláře na stejné úrovni jako zpracování signálů (metody handle*), tedy po action* metodě a před render* metodou.

Vykreslený formulář splňuje 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.

Data v proměnné $values neobsahují hodnoty formulářových tlačítek, takže je lze obvykle rovnou použít pro další zpracování (například vložení do databáze). Zároveň si můžete všimnout, že z textových políček jsou automaticky odstraněny levo- i pravostranné mezery. Schválně si zkuste do políčka napsat své jméno a za něj několik mezer – po odeslání budou mezery ořezané.

Zmínili jsme se o validaci, ale formulář zatím žádná validační pravidla nemá. Pojďme to napravit. Jméno bude povinné, proto je označíme metodou setRequired(), jejíž volitelný argument je text chybové hlášky, která se zobrazí, pokud uživatel jméno nevyplní:

$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 server vám jej bude nabízet do té doby, dokud jej nevyplníte v souladu s validačními pravidly. Formulář se automaticky validuje na straně klienta i na straně serveru.

Pokud nevycházíte z nette/sandbox, musíte pro zprovoznění JavaScript validace zalinkovat soubor netteForms.js, který najdete ve složce src/assets.

Nette Framework povinným prvkům nastaví CSS třídu required. Zkusme přidat stylopis

<style>
.required label { color: maroon }
</style>

a popiska „Jméno“ bude červená.

Označením povinných prvků validování pochopitelně nekončí. Přidáme další validační pravidla metodou addRule(), jejíž první argument říká, co chceme ověřovat, a druhý argument je opět text hlášky, která se zobrazí, pokud hodnota validací neprojde. Můžeme si vytvářet i vlastní validační pravidla, zatím si však vystačíme s předdefinovanými.

Formulář rozšíříme o nové políčko „věk“ s podmínkou, že je nepovinné, musí to být číslo (Form::INTEGER) a navíc v povoleném rozsahu (Form::RANGE). Zde využijeme třetí parametr metody addRule(), kterým předáme validátoru požadovaný rozsah:

$form->addText('age', 'Věk:')
    ->setRequired(false)
    ->addRule(Form::INTEGER, 'Věk musí být číslo')
    ->addRule(Form::RANGE, 'Věk musí být od 18 do 120', [18, 120]);

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 se musela přeložit do více jazyků, ztížila by se pozdější změna hodnot. Z toho důvodu je možné použít zástupné znaky v tomto formátu:

->addRule(Form::RANGE, 'Věk musí být od %d do %d let', [18, 120]);

Nette Framework podporuje HTML5 včetně nových formulářových prvků. Díky tomu můžeme políčko pro zadání věku označit jako číselné:

$form->addText('age', 'Věk:')
    ->setHtmlType('number')
    ...

V nejpokročilejších prohlížečích, jako je Chrome, Safari nebo Opera, se zobrazí šipečky pro snadnější změnu hodnoty, iPhone zobrazí optimalizovanou klávesnici s číslicemi.

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 znaky', 3);

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). Všimněte si dynamické odvolávky 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']);

Pokud by formulář nesloužil k registraci nových uživatelů, ale pro editaci záznamů, hodilo by se na začátku nastavit prvkům výchozí hodnoty.

Tímto máme hotový plně funkční formulář, který disponuje validací na straně klienta (tj. JavaScriptovou validací) i validací na straně serveru. Automaticky ošetřuje magic quotes, ověřuje, zda útočník neposílá nevalidní UTF-8 řetězce apod. Na tyto věci nemusíme myslet.

Příklady si můžete stáhnout. Zkuste si do něj přidat i další formulářové prvky. Inspiraci najdete také v distribuci v adresáři examples/Forms.

Formulářové prvky

Vedle široké škály vestavěných formulářových prvků můžete do formuláře přidávat vlastní prvky tímto způsobem:

$form = new Form;
$form['date'] = new DateInput('Datum:');

Pomocí extension method můžete doplnit formulář o novou metodu:

Nette\Forms\Container::extensionMethod('addZip', function ($form, $name, $label = null) {
    return $form->addText($name, $label)
        ->setRequired(false)
        ->addRule($form::PATTERN, '[0-9]{5}', 'Alespon 5 cisel');
});

$form = new Form;
$form->addZip('zip', 'ZIP code:');

Prvky se odstraní pomocí unset:

unset($form['zip');

Výchozí hodnoty

Nastavit výchozí hodnoty lze dvěma způsoby. Metodou setDefaults() nad celým formulářem nebo kontejnerem:

$form->addText('name', 'Jméno');
$form->addInteger('age', 'Věk');

$form->setDefaults([
    'name' => 'John',
    'age' => '33'
]);

nebo metodou setDefaultValue() nad prvkem:

$form->addEmail('email', 'E-mail')
    ->setDefaultValue('user@example.com');

U SelectBoxu nebo RadioListu zadáme jako výchozí hodnotu klíč z předaného pole hodnot:

$form->addSelect('country', 'Country', [
    'cz' => 'Česká republika',
    'sk' => 'Slovensko',
]);
$form['country']->setDefaultValue('sk');

U CheckBoxu:

$form->addCheckbox('agree', 'Agree with conditions')
    ->setDefaultValue(true);

Další užitečnou možností je použití „emptyValue“. Pokud je hodnota prvku po odeslání formuláře shodná s nastavenou „emptyValue“, tváří se prvek jako nevyplňený.

$form->addText('phone', 'Phone:')
    ->setEmptyValue('+42');

Deaktivace prvků

Pokud chceme některý prvek deaktivovat, můžeme využít metodu $control->setDisabled(true)

$form->addEmail('email', 'E-mail:')->setDisabled(true);

Do tohoto prvku nepůjde zapisovat a jeho hodnota nebude obsažena v datech vracených funkcí $form->getValues().

Pokud chceme prvek použít jen pro čtení, tj. aby se nastavená hodnota prvku zobrazila, ale prvek byl neaktivní, je potřeba nejdřív prvek deaktivovat a poté mu nastavit hodnotu. Je to z toho důvodu, že metoda setDisabled() hodnotu prvku vynuluje.

$form->addText('readonly', 'Readonly:')->setDisabled()->setValue('readonly value');

Pokud potřebujeme prvek pouze vyjmout z těchto dat, použijeme funkci $control->setOmitted(true). To se hodí pro různá hesla pro kontrolu, antispamové prvky atd.

$form->addText('antispam', 'Antispam:')->setOmitted(true);

Low-level formuláře

Lze používat i prvky, které zapíšeme pouze v šabloně a nepřidáme je do formuláře některou z metod $form->addXyz(). Když například vypisujeme záznamy z databáze a dopředu nevíme, kolik jich bude a jaké budou mít ID, a chceme u každého řádku zobrazit checkbox nebo radio button, stačí jej nakódovat v šabloně:

{foreach $items as $item}
    <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p>
{/foreach}

A po odeslání hodnotu zjistíme:

$values = $form->getHttpData($form::DATA_TEXT, 'sel[]');
$values = $form->getHttpData($form::DATA_TEXT | $form::DATA_KEYS, 'sel[]');

kde první parametr je typ elementu (DATA_FILE pro type=file, DATA_LINE pro jednořádkové vstupy jako text, password, email apod. a DATA_TEXT pro všechny ostatní) a druhý parametr sel[] odpovídá HTML atributu name. Typ elementu můžeme kombinovat s hodnotou DATA_KEYS, která zachová klíče prvků. To se hodí zejména pro select, radioList a checkboxList.

Podstatné je, že getHttpData() vrací sanitizovanou hodnotu, v tomto případě to bude vždy pole validních UTF-8 řetězců, ať už se pokusíte serveru podstrčit cokoliv. Jde o obdobu přímé práce s $_POST nebo $_GET avšak s tím podstatným rozdílem, že vždy vrací čistá data, tak, jak jste zvyklí u standardních prvků Nette formulářů.

Obrana před Cross-Site Request Forgery (CSRF)

Nette Framework ochrání vaše aplikace před útokem Cross-Site Request Forgery (CSRF). Útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně 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.

Ochrana je velmi snadná:

$form->addProtection('Vypršel časový limit, odešlete formulář znovu');

Proti útoku se lze bránit generováním a ověřováním autorizačního tokenu. Ten má platnost po dobu existence session. Díky tomu nebrání použití ve více oknech najednou (v rámci jedné session). Platnost je však možné zkrátit na počet sekund, které se uvedou jako druhý parametr. První parametr je přitom text chybové hlášky, která se zobrazí uživateli, pokud token vypršel.

Obrana by měla být aktivována pro všechny formuláře, které mění citlivá data v aplikaci.

Formuláře v presenterech

V presenterech se místo třídy Nette\Forms\Form používá od ní odvozená třída Nette\Application\UI\Form.

Použití stejného formuláře ve více presenterech

Pokud potřebujete jeden formulář použít ve více presenterech, máte dvě možnosti:

  1. vložit do hierarchie presenterů jejich společného předka a továrnu definovat tam
  2. nebo definovat formulář v samostatné tovární třídě a v jednotlivých továrnách vytvářet jeho instance.

Vhodné umístění pro takovou třídu je např. app/forms/SignInFormFactory.php. Naše tovární třída bude vypadat takto:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Jméno:');
        // ...
        $form->addSubmit('login', 'Přihlásit se');

        return $form;
    }
}

V továrničce každého presenteru, který náš formulář používá, jej následně vytvoříme voláním metody create():

protected function createComponentSignInForm()
{
    $form = (new SignInFormFactory())->create();
    $form['login']->caption = 'Pokračovat'; // můžeme také formulář pozměnit

    $form->onSuccess[] = [$this, 'signInFormSubmitted']; // a přidat událost po odeslání

    return $form;
}

Odeslaný formulář ale můžeme také zpracovávat na jediném místě. Do definice formuláře přesuneme volání událostí i s metodou signInFormSubmitted a přejmenujeme ji například na submitted, případně použijeme anonymní funkci:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Jméno:');
        ...
        $form->addSubmit('login', 'Přihlásit se');

        $form->onSuccess[] = function (Form $form, \stdClass $values) {
            // zde provedeme zpracování formuláře
        };

        return $form;
    }
}

Odesílání formuláře

Pokud má formulář více než jedno tlačítko, mezi kterými chceme rozlišovat, je vhodnější nastavit handler na událost onClick tlačítka. Ten se volá před handlerem události onSuccess:

$form->addSubmit('login', 'Přihlásit se')
    ->onClick[] = [$this, 'signInFormSubmitted'];

Když se formulář odešle tlačítkem enter, za odesílací tlačítko se považuje to první.

Handlery událostí onSuccess a onClick se volají pouze v případě, že je odeslání validní. Uvnitř obslužných metod tedy nemusíme validitu ověřovat. Formulář má ještě událost onSubmit, která se volá vždy nezávisle na validitě.

Samostatné použití Nette Forms

Pokud z nějakého důvodu nechcete používat celý framework, můžete využít Nette Forms samostatně. Vytvoření formuláře potom vypadá asi takto:

use Nette\Forms\Form;

$form = new Form;

$form->addText('name', 'Jméno:');
$form->addPassword('password', 'Heslo:');
$form->addSubmit('send', 'Registrovat');

echo $form; // vykreslí formulář

Takto vytvořený formulář se metodou POST odešle na stejnou stránku. To se dá snadno změnit:

$form = new Form;
$form->setAction('/submit.php');
$form->setMethod('GET');
...

Teď formulář oživíme. Dotazem na $form->isSuccess() zjistíme, zda byl formulář odeslán a zda byl vyplněn korektně. Pokud bude formulář správně vyplněn, data vypíšeme do okna prohlížeče. Za definici formuláře tedy vložíme kód:

if ($form->isSuccess()) {
    echo 'Formulář byl správně vyplněn a odeslán';

    $values = $form->getValues();
    dump($values);
}

K jednotlivým prvkům formuláře $form lze přistupovat pomocí hranatých závorek, podobně jako k prvkům pole. Takže třeba pod $form['name'] se skrývá objekt Nette\Forms\Controls\TextInput představující první políčko formuláře.

Po odeslání a zpracování formuláře je vhodné přesměrovat na další stránku. Zabrání se tak nechtěnému opětovnému odeslání formuláře tlačítkem Obnovit nebo Zpět.

Vykreslení formuláře

Každý prvek disponuje metodami getLabel() a getControl(), které vracejí HTML kód popisky a samotného prvku. Nette Framework dovoluje ke getterům přistupovat podobně, jako by to byly proměnné, takže stačí psát jen label a control.

<?php $form->render('begin') ?>
<?php $form->render('errors') ?>

<table>
<tr class="required">
    <th><?php echo $form['name']->label // Zavolá getLabel() ?></th>
    <td><?php echo $form['name']->control // Zavolá getControl() ?></td>
</tr>

<tr class="required">
    <th><?php echo $form['age']->label ?></th>
    <td><?php echo $form['age']->control ?></td>
</tr>

...

</table>

<?php $form->render('end') ?>