Validace formulářů

Povinné prvky

Povinné prvky označíme metodou setRequired(), jejíž argument je text chybové hlášky, která se zobrazí, pokud uživatel prvek nevyplní. Pokud argument neuvedeme, použije se výchozí chybová hláška.

$form->addText('name', 'Jméno:')
	->setRequired('Zadejte prosím jméno');

Pravidla

Validační pravidla přidáváme prvkům metodou addRule(). První parametr je pravidlo, druhý je text chybové hlášky a třetí je argument validačního pravidla.

$form->addPassword('password', 'Heslo:')
	->addRule($form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaků', 8);

Validační pravidla se ověřují pouze v případě, že uživatel prvek vyplnil.

Nette přichází s celou řadu předdefinovaných pravidel, jejichž názvy jsou konstanty třídy Nette\Forms\Form. U všech prvků můžeme použít tyto pravidla:

konstanta popis typ argumentu
REQUIRED povinný prvek, alias pro setRequired()
FILLED povinný prvek, alias pro setRequired()
BLANK prvek nesmí být vyplněn
EQUAL hodnota se rovná parametru mixed
NOT_EQUAL hodnota se nerovná parametru mixed
IS_IN hodnota se rovná některé položce v poli array
IS_NOT_IN hodnota se nerovná žádné položce v poli array
VALID je prvek vyplněn správně? (pro podmínky)

Textové vstupy

U prvků addText(), addPassword(), addTextArea(), addEmail(), addInteger(), lze použít i některá následující pravidla:

MIN_LENGTH minimální délka textu int
MAX_LENGTH maximální délka textu int
LENGTH délka v rozsahu nebo přesná délka dvojice [int, int] nebo int
EMAIL platná e-mailová adresa
URL absolutní URL
PATTERN vyhovuje regulárnímu výrazu string
PATTERN_ICASE jako PATTERN, ale nezávislé na velikosti písmen; od nette/forms v2.4.9 string
INTEGER celočíselná hodnota
NUMERIC alias pro INTEGER
FLOAT číslo
MIN minimální hodnota číselného prvku int|float
MAX maximální hodnota číselného prvku int|float
RANGE hodnota v rozsahu dvojice [int|float, int|float]

Validační pravidla INTEGER, NUMERIC a FLOAT rovnou převádí hodnotu na integer resp. float. A dále pravidlo URL akceptuje i adresu bez schématu (např. nette.org) a schéma doplní (http://nette.org). Výraz v PATTERN a PATTERN_ICASE musí platit pro celou hodnotu, tj. jako by byl obalen znaky ^ a $.

Počet položek

U prvků addMultiUpload(), addCheckboxList(), addMultiSelect() lze použít i následující pravidla pro omezení počtu vybraných položek resp. uploadovaných souborů:

MIN_LENGTH minimální počet int
MAX_LENGTH maximální počet int
LENGTH počet v rozsahu nebo přesný počet dvojice [int, int] nebo int

Upload souborů

U prvků addUpload(), addMultiUpload() lze použít i následující pravidla:

MAX_FILE_SIZE maximální velikost souboru int
MIME_TYPE MIME type, povoleny zástupné znaky ('video/*') string|string[]
IMAGE obrázek JPEG, PNG, GIF, WebP
PATTERN jméno souboru vyhovuje regulárnímu výrazu; od nette/forms v2.4.9 string
PATTERN_ICASE jako PATTERN, ale nezávislé na velikosti písmen; od nette/forms v2.4.9 string

MIME_TYPE a IMAGE vyžadují PHP rozšíření fileinfo. Že je soubor či obrázek požadovaného typu detekují na základě jeho signatury a neověřují integritu celého souboru. Zda není obrázek poškozený lze zjistit například pokusem o jeho načtení.

Chybové hlášky

Všechny předdefinované pravidla s výjimkou PATTERN a PATTERN_ICASE mají výchozí chybovou hlášku, takže ji lze vynechat. Nicméně uvedením a formulací všech hlášek na míru uděláte formulář uživatelsky přívětivější.

Změnit výchozí hlášky můžete v konfiguraci, úpravou textů v poli Nette\Forms\Validator::$messages nebo použitím translatoru.

V textu chybových hlášek lze používat tyto zástupné řetězce:

%d nahradí postupně za argumenty pravidla
%n$d nahradí za n-tý argument pravidla
%label nahradí za popisku prvku (bez dvojtečky)
%name nahradí za jméno prvku (např. name)
%value nahradí za uživatelem vloženou hodnotu
$form->addText('name', 'Jméno:')
	->setRequired('Vyplňte prosím %label');

$form->addInteger('id', 'ID:')
	->addRule($form::RANGE, 'nejméně %d a nejvíce %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::RANGE, 'nejvíce %2$d a nejméně %1$d', [5, 10]);

Podmínky

Kromě pravidel lze přidávat také podmínky. Ty se zapisují podobně jako pravidla, jen místo addRule() použijeme metodu addCondition() a pochopitelně neuvádíme žádnou chybovou zprávu (podmínka se jen ptá):

$form->addPassword('password', 'Heslo:')
	// pokud není heslo delší než 8 znaků
	->addCondition($form::MAX_LENGTH, 8)
		// pak musí obsahovat číslici
		->addRule($form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*');

Podmínku je možné vázat i na jiný prvek než aktuální pomocí addConditionOn(). Jako první parametr uvedeme referenci na prvek. V této ukázce bude e-mail povinný jen tehdy, zaškrtne-li se checkbox (jeho hodnota bude true):

$form->addCheckbox('newsletters', 'zasílejte mi newslettery');

$form->addEmail('email', 'E-mail:')
	// pokud je checkbox zaškrtnut
	->addConditionOn($form['newsletters'], $form::EQUAL, true)
		// pak vyžaduj e-mail
		->setRequired('Zadejte e-mailovou adresu');

Z podmínek lze vytvářet komplexní struktury za pomoci elseCondition() a endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // je-li splněna první podmínka
		->addConditionOn(/* ... */) // a druhá podmínka na jiném prvku
			->addRule(/* ... */) // vyžaduj toto pravidlo
		->elseCondition() // pokud druhá podmínka splněná není
			->addRule(/* ... */) // vyžaduj tato pravidla
			->addRule(/* ... */)
		->endCondition() // vracíme se k první podmínce
		->addRule(/* ... */);

Podmínky je možné negovat znakem ~ (vlnovka), tj. addCondition(~$form::NUMBER, ...), což je v Nette 3 zavržené, takže je potřeba použít else-podmínku.

V Nette lze velmi snadno reagovat na splnění či nesplnění podmíny i na straně JavaScriptu pomocí metody toggle(), viz dynamický JavaScript.

Reference mezi prvky

Jako argument pravidla či podmínky lze uvádět referenci na jiný prvek. Takto lze např. dynamicky validovat, že prvek text má tolik znaků, jako je hodnota prvku length:

$form->addInteger('length');
$form->addText('text')
	->addRule($form::LENGTH, null, $form['length']);

Vlastní pravidla a podmínky

Občas se dostaneme do situace, kdy nám vestavěná validační pravidla v Nette nestačí a potřebujeme data od uživatele validovat po svém. V Nette je to velmi jednoduché!

Metodám addRule() či addCondition() lze první parametr předat libovolný callback. Ten přijímá jako první parametr samotný prvek a vrací boolean hodnotu určující, zda validace proběhla v pořádku. Při přidávání pravidla pomocí addRule() je možné zadat i další argumenty, ty jsou pak předány jako druhý parametr.

Vlastní sadu validátorů tak můžeme vytvořit jako třídu se statickými metodami:

class MyValidators
{
	// testuje, zda je hodnota dělitelná argumentem
	public static function validateDivisibility(BaseControl $input, $arg)
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// další validátory
	}
}

Použití je pak velmi jednoduché:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'Hodnota musí být násobkem čísla %d',
		8
	);

Vlastní validační pravidla lze přidávat i do JavaScriptu. Podmínkou je, aby pravidlo byla statická metoda. Její název pro JavaScriptový validátor vznikne spojením názvu třídy bez zpětných lomítek \, podtržítka _ a názvu metody. Např. App\MyValidators::validateDivisibility zapíšeme jako AppMyValidators_validateDivisibility a přidáme do objektu Nette.validators:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Událost onValidate

Po odeslání formuláře se provádí validace, kdy se zkontrolují jednotlivá pravidla přidaná pomocí addRule() a následně se vyvolá událost onValidate. Její handler lze využít k doplňkové validaci, typicky ověření správné kombinace hodnot ve více prvcích formuláře.

Pokud se odhalí chyba, předáme ji do formuláře metodou addError(). Tu lze volat buď na konkrétním prvku, nebo přímo na formuláři.

protected function createComponentSignInForm()
{
	$form = new Form;
	// ...
	$form->onValidate[] = [$this, 'validateSignInForm'];
	return $form;
}

public function validateSignInForm(Form $form, \stdClass $data)
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('Tato kombinace není možná.');
	}
}

Chyby při zpracování

V mnoha případech se o chybě dozvíme až ve chvíli, kdy zpracováváme platný formulář, například zapisujeme novou položku do databáze a narazíme na duplicitu klíčů. V takovém případě chybu opět předáme do formuláře metodou addError(). Tu lze volat buď na konkrétním prvku, nebo přímo na formuláři:

try {
	$data = $form->getValues();
	$this->user->login($data->username, $data->password);
	$this->redirect('Homepage:');

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\IAuthenticator::INVALID_CREDENTIAL) {
		$form->addError('Neplatné heslo.');
	}
	// ...
}

Pokud je to možné, doporučujeme připojit chybu přímo k prvku formuláře, protože se zobrazí vedle něj při použití výchozího rendereru.

$form['date']->addError('Omlouváme se, ale toto datum již je zabrané.');

Můžete addError() volat opakovaně a tak předat formuláři nebo prvku více chybových zpráv. Získáte je pomocí getErrors().

Pozor, $form->getErrors() vrací sumář všech chybových zpráv, i těch, co byly předány přímo jednotlivým prvkům, nejen přímo formuláři. Chybové zprávy předané pouze formuláři získáte přes $form->getOwnErrors().

Úprava vstupu

Pomocí metody addFilter() můžeme pozměnit uživatelem vloženou hodnotu. V této ukázce budeme tolerovat a odstraňovat mezery v PSČ:

$form->addText('zip', 'PSČ:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // odstraníme mezery z PSČ
	})
	->addRule($form::PATTERN, 'PSČ není ve tvaru pěti číslic', '\d{5}');

Filtr se začlení mezi validační pravidla a podmínky a tedy záleží na pořadí metod, tj. filtr a pravidlo se volají v takovém pořadí, jako je pořadí metod addFilter() a addRule().

JavaScriptová validace

Jazyk pro formulování podmínek a pravidel je velice silný. Všechny konstrukce přitom fungují jak na straně serveru, tak i na straně JavaScriptu. Přenáší se v HTML atributech data-nette-rules jako JSON. Samotnou validaci pak provádí skript, který odchytí událost formuláře submit, projde jednotlivé prvky a vykoná příslušnou validaci.

Tím skriptem je netteForms.js a je dostupný z více možných zdrojů:

Skript můžete vložit přímo do HTML stránky ze CDN:

<script src="https://nette.github.io/resources/js/2/netteForms.min.js"></script>

Nebo zkopírovat lokálně do veřejné složky projektu (např. z vendor/nette/forms/src/assets/netteForms.min.js):

<script src="/path/to/netteForms.min.js"></script>

Nebo nainstalovat přes npm:

npm install nette-forms

A následně načíst a spustit:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternativně jej můžete načíst přímo ze složky vendor:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

Dynamický JavaScript

Chcete zobrazit políčka pro zadání adresy pouze pokud uživatel zvolí zboží zaslat poštou? Žádný problém. Klíčem je dvojice metod addCondition() & toggle():

$form->addCheckbox('send_it')
	->addCondition($form::EQUAL, true)
		->toggle('#address-container');

Tento kód říká, že když je podmínka splněná, tedy když je zaškrtnutý checkbox, bude viditelný HTML element #address-container. A obráceně. Prvky formuláře s adresou příjemce tak umístíme do kontejneru s tímto ID a při kliknutí na checkbox se skryjí nebo zobrazí. To zajišťuje skript netteForms.js.

Jako argument metody toggle() je možné předat libovolný selektor. Z historických důvodů se alfanumerický řetězec bez dalších speciálních znaků chápe jako ID prvku, tedy stejně, jako by mu předcházel znak #. Druhý nepovinný parametr umožňuje obrátit chování, tj. pokud bychom použili toggle('#address-container', false), element by se naopak zobrazil pouze tehdy, pokud by checkbox zaškrtnutý nebyl.

Výchozí implementace v JavaScriptu mění elementům property hidden. Chování však můžeme snadno změnit, například přidat animaci. Stačí v JavaScriptu přepsat metodu Nette.toggle vlastním řešením:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// skryjeme nebo zobrazime 'el' dle hodnoty 'visible'
	});
};

Vypnutí validace

Někdy se může hodit validaci vypnout. Pokud stisknutí odesílacího tlačítka nemá provádět validaci (vhodné pro tlačítka Cancel nebo Preview), vypneme ji metodou $submit->setValidationScope([]). Pokud má provádět validaci jen částečnou, můžeme určit které pole nebo formulářové kontejnery se mají validovat.

$form->addText('name')
	->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')
	->setRequired('age');
$details->addInteger('age2')
	->setRequired('age2');

$form->addSubmit('send1'); // Validuje celý formuláře
$form->addSubmit('send2')
	->setValidationScope([]); // Nevaliduje vůbec
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // Validuje pouze prvek name
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Validuje pouze prvek age
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Validuje kontejner details

setValidationScope neovlivní událost onValidate u formuláře, která bude zavolána vždy. Událost onValidate u kontejneru bude vyvolána pouze pokud je tento kontejner označen pro částečnou validaci.

verze: 4.0 3.x 2.x