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

Обязательные элементы

Обязательные элементы помечаются методом 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 или используя переводчик.

В тексте сообщений об ошибках можно использовать следующие заполнители (placeholders):

%d заменяется последовательно аргументами правила
%n$d заменяется n-м аргументом правила
%label заменяется меткой (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(). В качестве первого параметра указывается ссылка на элемент. В этом примере e-mail будет обязательным только тогда, когда установлен флажок (его значение будет true):

$form->addCheckbox('newsletters', 'присылайте мне рассылки');

$form->addEmail('email', 'E-mail:')
	// если флажок установлен
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// то требуй e-mail
		->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. Его обработчик можно использовать для дополнительной валидации, обычно для проверки правильной комбинации значений в нескольких элементах формы.

Если обнаружена ошибка, мы передаем ее в форму методом 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