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

Обязательные для заполнения элементы

Элементы управления помечаются как обязательные с помощью метода 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() также могут быть использованы следующие правила:

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 автоматически преобразуют значение в целое (или плавающее соответственно). Более того, правило URL также принимает адрес без схемы (например, nette.org) и дополняет схему (https://nette.org). Выражения в Pattern и PatternInsensitive должны быть действительны для всего значения, т. е. как если бы оно было обернуто в символы ^ и $.

Для элементов управления addUpload(), addMultiUpload() также могут быть использованы следующие правила:

MaxFileSize максимальный размер файла int
MimeType Тип MIME, принимает подстановочные знаки ('video/*') string|string[]
Image загруженный файл является JPEG, PNG, GIF, WebP
Pattern имя файла соответствует регулярному выражению string
PatternInsensitive как Pattern, но без учета регистра. string

Для MimeType и Image требуется расширение PHP fileinfo. Принадлежность файла или изображения к нужному типу определяется по его сигнатуре. Целостность всего файла не проверяется. Вы можете узнать, не повреждено ли изображение, например, попытавшись загрузить его.

Для элементов управления addMultiUpload(), addCheckboxList(), addMultiSelect() следующие правила также могут быть использованы для ограничения количества выбранных элементов, соответственно загруженных файлов:

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

Сообщения об ошибках

Все предопределенные правила, кроме Pattern и PatternInsensitive, имеют сообщение об ошибке по умолчанию, поэтому их можно опустить. Однако, передав и сформулировав все индивидуальные сообщения, вы сделаете форму более удобной для пользователя.

Вы можете изменить сообщения по умолчанию в configuration, изменяя тексты в массиве Nette\Forms\Validator::$messages или используя translator.

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

%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.

Ссылки между элементами управления

Аргумент правила или условия может быть ссылкой на другой элемент. Например, вы можете динамически подтвердить, что text имеет столько символов, сколько указано в поле length:

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

Пользовательские правила и условия

Иногда мы сталкиваемся с ситуацией, когда встроенных правил валидации в Nette недостаточно, и нам нужно проверить данные от пользователя по-своему. В Nette это очень просто!

Вы можете передать любой обратный вызов в качестве первого параметра в методы addRule() или addCondition(). Обратный вызов принимает сам элемент в качестве первого параметра и возвращает булево значение, указывающее на успешность проверки. При добавлении правила с помощью 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('Homepage:');

} 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://nette.github.io/resources/js/3/netteForms.min.js"></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(). По историческим причинам буквенно-цифровая строка без других специальных символов рассматривается как идентификатор элемента, так же как если бы ей предшествовал символ #. Второй необязательный параметр позволяет нам изменить поведение, т. е. если бы мы использовали toggle('#address-container', false), элемент отображался бы только при снятом флажке.

Реализация JavaScript по умолчанию изменяет свойство hidden для элементов. Однако мы можем легко изменить поведение, например, добавив анимацию. Просто переопределите метод Nette.toggle в JavaScript с помощью собственного решения:

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

Отключение валидации

В некоторых случаях необходимо отключить валидацию. Если кнопка submit не должна выполнять проверку после отправки (например, кнопка Отмена или Предварительный просмотр), вы можете отключить проверку, вызвав $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'|doc:name]]); // Проверяет только поле 'имя'
$form->addSubmit('send4')
	->setValidationScope([$form['details'|doc:details]['age'|/age]]); // Проверяется только поле 'возраст'
$form->addSubmit('send5')
	->setValidationScope([$form['details'|doc:details]]); // Проверяет контейнер 'details'

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