Валидация на форми

Задължителни елементи

Задължителните елементи маркираме с метода setRequired(), чийто аргумент е текстът на Съобщения за грешки, който ще се покаже, ако потребителят не попълни елемента. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране.

$form->addText('name', 'Име:')
	->setRequired('Моля, въведете име');

Правила

Правилата за валидация добавяме към елементите с метода addRule(). Първият параметър е правилото, вторият е текстът на Съобщения за грешки и третият е аргументът на правилото за валидация.

$form->addPassword('password', 'Парола:')
	->addRule($form::MinLength, 'Паролата трябва да има поне %d знака', 8);

Правилата за валидация се проверяват само в случай, че потребителят е попълнил елемента.

Nette идва с цяла редица предварително дефинирани правила, чиито имена са константи на класа Nette\Forms\Form. При всички елементи можем да използваме тези правила:

константа описание тип аргумент
Required задължителен елемент, псевдоним за setRequired()
Filled задължителен елемент, псевдоним за setRequired()
Blank елементът не трябва да бъде попълнен
Equal стойността е равна на параметъра mixed
NotEqual стойността не е равна на параметъра mixed
IsIn стойността е равна на някой елемент в масива array
IsNotIn стойността не е равна на никой елемент в масива array
Valid елементът попълнен ли е правилно? (за Условия)

Текстови полета

При елементите addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() могат да се използват и някои от следните правила:

MinLength минимална дължина на текста int
MaxLength максимална дължина на текста int
Length дължина в диапазон или точна дължина двойка [int, int] или int
Email валиден имейл адрес
URL абсолютен URL
Pattern съответства на регулярен израз string
PatternInsensitive като Pattern, но независимо от големината на буквите string
Integer целочислена стойност
Numeric псевдоним за Integer
Float число
Min минимална стойност на числов елемент int|float
Max максимална стойност на числов елемент int|float
Range стойност в диапазон двойка [int|float, int|float]

Правилата за валидация Integer, Numeric и Float веднага преобразуват стойността в integer съответно float. Освен това правилото URL приема и адрес без схема (напр. nette.org) и допълва схемата (https://nette.org). Изразът в Pattern и PatternIcase трябва да важи за цялата стойност, т.е. сякаш е обгърнат със знаците ^ и $.

Брой елементи

При елементите addMultiUpload(), addCheckboxList(), addMultiSelect() могат да се използват и следните правила за ограничаване на броя на избраните елементи съответно качените файлове:

MinLength минимален брой int
MaxLength максимален брой int
Length брой в диапазон или точен брой двойка [int, int] или int

Качване на файлове

При елементите addUpload(), addMultiUpload() могат да се използват и следните правила:

MaxFileSize максимален размер на файла в байтове int
MimeType MIME тип, разрешени са заместващи знаци ('video/*') string|string[]
Image изображение JPEG, PNG, GIF, WebP, AVIF
Pattern името на файла съответства на регулярен израз string
PatternInsensitive като Pattern, но независимо от големината на буквите string

MimeType и Image изискват PHP разширението fileinfo. Дали файлът или изображението е от изисквания тип се открива въз основа на неговата сигнатура и не се проверява целостта на целия файл. Дали изображението не е повредено може да се установи например чрез опит за неговото зареждане.

Съобщения за грешки

Всички предварително дефинирани правила с изключение на Pattern и PatternInsensitive имат съобщение за грешка по подразбиране, така че то може да бъде пропуснато. Въпреки това, като посочите и формулирате всички съобщения по мярка, ще направите формата по-удобна за потребителя.

Можете да промените съобщенията по подразбиране в конфигурацията, като редактирате текстовете в масива Nette\Forms\Validator::$messages или като използвате преводач.

В текста на съобщенията за грешки могат да се използват следните заместващи низове:

%d заменя последователно с аргументите на правилото
%n$d заменя с n-тия аргумент на правилото
%label заменя с надписа на елемента (без двоеточие)
%name заменя с името на елемента (напр. name)
%value заменя с въведената от потребителя стойност
$form->addText('name', 'Име:')
	->setRequired('Моля, попълнете %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'поне %d и най-много %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'най-много %2$d и поне %1$d', [5, 10]);

Условия

Освен правила могат да се добавят и условия. Те се записват подобно на правилата, само че вместо addRule() използваме метода addCondition() и разбира се не посочваме никакво съобщение за грешка (условието само пита):

$form->addPassword('password', 'Парола:')
	// ако паролата не е по-дълга от 8 знака
	->addCondition($form::MaxLength, 8)
		// тогава трябва да съдържа цифра
		->addRule($form::Pattern, 'Трябва да съдържа цифра', '.*[0-9].*');

Условието може да бъде обвързано и с друг елемент освен текущия с помощта на addConditionOn(). Като първи параметър посочваме референция към елемента. В този пример имейлът ще бъде задължителен само тогава, когато се отметне чекбоксът (неговата стойност ще бъде true):

$form->addCheckbox('newsletters', 'изпращайте ми бюлетини');

$form->addEmail('email', 'Имейл:')
	// ако чекбоксът е отметнат
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// тогава изисквай имейл
		->setRequired('Въведете имейл адрес');

От условията могат да се създават комплексни структури с помощта на elseCondition() и endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // ако е изпълнено първото условие
		->addConditionOn(/* ... */) // и второто условие на друг елемент
			->addRule(/* ... */) // изисквай това правило
		->elseCondition() // ако второто условие не е изпълнено
			->addRule(/* ... */) // изисквай тези правила
			->addRule(/* ... */)
		->endCondition() // връщаме се към първото условие
		->addRule(/* ... */);

В Nette може много лесно да се реагира на изпълнението или неизпълнението на условие и от страна на JavaScript с помощта на метода toggle(), виж Динамичен JavaScript.

Референция към друг елемент

Като аргумент на правило или условие може да се предаде и друг елемент на формата. Правилото тогава ще използва стойността, въведена по-късно от потребителя в браузъра. Така може например динамично да се валидира, че елементът password съдържа същия низ като елемента password_confirm:

$form->addPassword('password', 'Парола');
$form->addPassword('password_confirm', 'Потвърдете паролата')
    ->addRule($form::Equal, 'Въведените пароли не съвпадат', $form['password']);

Персонализирани правила и условия

Понякога се озоваваме в ситуация, когато вградените правила за валидация в Nette не са достатъчни и трябва да валидираме данните от потребителя по свой начин. В Nette това е много лесно!

На методите addRule() или addCondition() може да се предаде като първи параметър произволен callback. Той приема като първи параметър самия елемент и връща булева стойност, определяща дали валидацията е преминала успешно. При добавяне на правило с addRule() е възможно да се зададат и други аргументи, те след това се предават като втори параметър.

Така можем да създадем собствен набор от валидатори като клас със статични методи:

class MyValidators
{
	// проверява дали стойността се дели на аргумента
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// други валидатори
	}
}

Използването след това е много лесно:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'Стойността трябва да е кратна на %d',
		8,
	);

Персонализирани правила за валидация могат да се добавят и в JavaScript. Условието е правилото да бъде статичен метод. Неговото име за JavaScript валидатора се образува чрез свързване на името на класа без обратни наклонени черти \, долна черта _ и името на метода. Напр. App\MyValidators::validateDivisibility записваме като AppMyValidators_validateDivisibility и добавяме към обекта Nette.validators:

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

Събитие onValidate

След изпращане на формата се извършва валидация, при която се проверяват отделните правила, добавени с addRule(), и след това се извиква събитие onValidate. Неговият handler може да се използва за допълнителна валидация, типично проверка на правилната комбинация от стойности в няколко елемента на формата.

Ако се открие грешка, я предаваме на формата с метода addError(). Той може да се извика или на конкретен елемент, или директно на формата.

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

public function validateSignInForm(Form $form, \stdClass $data): void
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('Тази комбинация не е възможна.');
	}
}

Грешки при обработка

В много случаи научаваме за грешката едва когато обработваме валидната форма, например записваме нов елемент в базата данни и се натъкваме на дублиране на ключове. В такъв случай отново предаваме грешката на формата с метода addError(). Той може да се извика или на конкретен елемент, или директно на формата:

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

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) {
		$form->addError('Невалидна парола.');
	}
}

Ако е възможно, препоръчваме да прикачите грешката директно към елемента на формата, тъй като тя ще се покаже до него при използване на рендеръра по подразбиране.

$form['date']->addError('Извиняваме се, но тази дата вече е заета.');

Можете да извиквате addError() многократно и така да предавате на формата или елемента повече съобщения за грешки. Получавате ги с getErrors().

Внимание, $form->getErrors() връща резюме на всички съобщения за грешки, включително тези, които са били предадени директно на отделни елементи, не само директно на формата. Съобщенията за грешки, предадени само на формата, получавате чрез $form->getOwnErrors().

Модификация на входа

С помощта на метода addFilter() можем да променим въведената от потребителя стойност. В този пример ще толерираме и премахваме интервали в пощенския код:

$form->addText('zip', 'Пощенски код:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // премахваме интервалите от пощенския код
	})
	->addRule($form::Pattern, 'Пощенският код не е във формат от пет цифри', '\d{5}');

Филтърът се интегрира между правилата за валидация и условията и следователно редът на методите има значение, т.е. филтърът и правилото се извикват в такъв ред, какъвто е редът на методите addFilter() и addRule().

JavaScript валидация

Езикът за формулиране на условия и правила е много мощен. Всички конструкции при това работят както от страна на сървъра, така и от страна на JavaScript. Пренасят се в HTML атрибути data-nette-rules като JSON. Самата валидация след това се извършва от скрипт, който прихваща събитието submit на формата, преминава през отделните елементи и извършва съответната валидация.

Този скрипт е netteForms.js и е достъпен от няколко възможни източника:

Можете да вмъкнете скрипта директно в HTML страницата от CDN:

<script src="https://unpkg.com/nette-forms@3"></script>

Или да го копирате локално в публичната папка на проекта (напр. от vendor/nette/forms/src/assets/netteForms.min.js):

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

Или да го инсталирате чрез npm:

npm install nette-forms

И след това да го заредите и стартирате:

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

Алтернативно можете да го заредите директно от папката vendor:

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

Динамичен JavaScript

Искате да покажете полетата за въвеждане на адрес само ако потребителят избере стоката да бъде изпратена по пощата? Няма проблем. Ключът е двойката методи addCondition() & toggle():

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

Този код казва, че когато условието е изпълнено, т.е. когато чекбоксът е отметнат, ще бъде видим HTML елементът #address-container. И обратното. Елементите на формата с адреса на получателя така поставяме в контейнер с това ID и при кликване върху чекбокса те се скриват или показват. Това осигурява скриптът netteForms.js.

Като аргумент на метода toggle() може да се предаде произволен селектор. По исторически причини буквено-цифров низ без други специални знаци се разбира като ID на елемент, т.е. същото, като че ли му предхожда знакът #. Вторият незадължителен параметър позволява да се обърне поведението, т.е. ако използваме toggle('#address-container', false), елементът ще се покаже обратно само тогава, ако чекбоксът не е отметнат.

Имплементацията по подразбиране в JavaScript променя свойството hidden на елементите. Поведението обаче можем лесно да променим, например да добавим анимация. Достатъчно е в JavaScript да презапишем метода Nette.toggle със собствено решение:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// скриваме или показваме 'el' според стойността на 'visible'
	});
};

Изключване на валидацията

Понякога може да се наложи да изключите валидацията. Ако натискането на бутона за изпращане не трябва да извършва валидация (подходящо за бутони Cancel или Preview), я изключваме с метода $submit->setValidationScope([]). Ако трябва да извършва само частична валидация, можем да определим кои полета или контейнери на формата трябва да се валидират.

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

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

$form->addSubmit('send1'); // Валидира цялата форма
$form->addSubmit('send2')
	->setValidationScope([]); // Не валидира изобщо
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // Валидира само елемента name
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Валидира само елемента age
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Валидира контейнера details

setValidationScope не влияе на Събитие onValidate на формата, която ще бъде извикана винаги. Събитието onValidate на контейнера ще бъде извикано само ако този контейнер е маркиран за частична валидация.

версия: 4.0