Validace formulářů

Validace formulářových prvků

Na formulářové prvky lze kromě validačních pravidel uvedených v přehledu prvků používat ještě tyto pravidla:

Form::FILLED je prvek vyplněn?
Form::REQUIRED alias pro Form::FILLED
Form::EQUAL je hodnota rovna uvedené? mixed $value nebo $values[]
Form::NOT_EQUAL prvek se nesmí rovnat zadané hodnotě mixed $value nebo $values[]
Form::IS_IN testuje, zda hodnota spadá do výčtu mixed $value nebo $values[]
Form::IS_NOT_IN hodnota nesmí spadat do výčtu mixed $value nebo $values[]
Form::VALID je prvek vyplněn správně?  
Form::BLANK prvek nesmí být vyplněn  

Všem validačním pravidlům můžeme přidat vlastní chybovou hlášku, nebo se použije hláška výchozí, kterou můžete případně přepsat. U vícejazyčných formulářů se hlášky automaticky překládají.

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

%label nahradí se textem popisky
%name nahradí se identifikátorem prvku
%value nahradí se zadanou hodnotou

Kromě pravidel lze přidávat také dotazovací 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ž 5 znaků
	->addCondition(Form::MAX_LENGTH, 5)
		// pak bude muset obsahovat číslici
		->addRule(Form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*');

Podmínku je možné vázat i na jiný prvek, než ten aktuální. Stačí addCondition() nahradit za addConditionOn() a jako první parametr uvést odvolávku na jiný prvek. V tomto případě se bude e-mail vyžadovat tehdy, zaškrtne-li se checkbox (tj. jeho logická 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');

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 jiné odpovídající pravidlo (např. opakem FILLED je BLANK).

Také lze z podmínek vytvářet komplexní struktury za pomoci metod elseCondition() a endCondition().

Jak vidíte, 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.

Můžeme si také přidat vlastní validátory. Metody addRule() a addCondition() totiž jako název pravidla akceptují callback:

// uživatelský validátor: testuje, zda je hodnota dělitelná argumentem
// poznámka: toto je skutečná funkce, nikoliv metoda v presenteru
function divisibilityValidator(BaseControl $item, $arg)
{
	return $item->getValue() % $arg === 0;
}

$form->addInteger('number', 'Číslo:')
	->addRule('divisibilityValidator', 'Číslo musí být dělitelné %d.', 8);

JavaScript

Validační pravidla se na stranu JavaScriptu přenášejí v HTML atributech data-nette-rules, které obsahují JSON popisující jednotlivá pravidla nebo podmínky. Samotnou validaci pak provádí skript, který odchytí událost submit, projde jednotlivé prvky a vykoná příslušnou validaci. Výchozí implementací je soubor netteForms.js, který najdete v distribuci v adresáři src/assets. Stačí jej tedy do stránky zalinkovat:

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

Vlastní validační pravidla přidáme rozšířením objektu Nette.validators:

<script>
Nette.validators.divisibilityValidator = function(elem, args, val) {
	return val % args === 0;
};
</script>

Pokud náš validační callback v PHP je statická metoda ve třídě, tak název pro JavaScriptový validátor vytvoříme smazáním zpětných lomítek \ a nahrazením dvojité dvojtečky za jedno podtržítko _, např. App\MyValidator::divisibilityValidator zapíšeme jako AppMyValidator_divisibilityValidator.

Úprava hodnot

Pomocí metody addFilter můžeme upravit hodnotu ještě před samotným zpracování formuláře. addFilter je možno kombinovat s metodami addCondition a addConditionOn.

$form->addText('zip', 'PSČ:')
	->addCondition($form::FILLED)
	->addFilter(function ($value) {
		return str_replace(' ', '', $value);
	});

Když poté přístoupíme ve zpracování formuláře k jeho hodnotám, PSČ již bude zbaveno mezer.

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 zpětně předáme do formuláře metodou addError(). Tu lze volat buď na konkrétním prvku, nebo přímo na formuláři:

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

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

Doporučuje se 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é.");

Validace celého formuláře

Pokud z nějakého důvodu potřebujete přidat validační funkcionalitu, typicky ověření správné kombinace hodnot ve více prvcích formuláře, můžete si napsat vlastní validační funkce. Ty pak na formulář navážete pomocí události onValidate:

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

public function validateSignInForm($form)
{
	$values = $form->getValues();

	if (...) { // validační podmínka
		$form->addError('Tato kombinace není možná.');
	}
}

Na událost onValidate můžete registrovat libovolný počet funkcí. Funkce je chápána jako úspěšná, pokud nepřidá do formuláře chybu pomocí $form->addError().

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(false); // Nevaliduje vůbec
$form->addSubmit('send3')->setValidationScope([$form['name']]); // Validuje pouze pole name
$form->addSubmit('send4')->setValidationScope([$form['details']['age']]); // Validuje pouze pole 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.

Související články na blogu