Проверка форм

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

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

Текстовые поля

К элементам addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() можно применить и некоторые из следующих правил:

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

Правила проверки Integer и Float автоматически преобразуют значение соответственно в целое или дробное число. Кроме того, правило URL принимает и адрес без схемы (например, nette.org) и схему дополняет (https://nette.org). Выражение в Pattern и PatternInsensitive должно быть верным для всего значения, то есть как если бы оно было обёрнуто в символы ^ и $.

Количество элементов

Для элементов 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(). Первый параметр – ссылка на элемент. В этом примере email будет обязателен, только если отмечен флажок (то есть его значение true):

$form->addCheckbox('newsletters', 'Присылать мне новости');

$form->addEmail('email', 'Email:')
	// если флажок отмечен
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// то требовать email
		->setRequired('Введите ваш адрес электронной почты');

Из условий можно строить сложные конструкции с помощью elseCondition() и endCondition():

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

Первым аргументом addCondition() может быть и логическое значение. Это удобно, когда решение известно уже при построении формы, например чтобы применить правило только при определённых обстоятельствах:

$form->addText('nickname')
	->addCondition($isRequired) // значение, известное при построении формы
		->setRequired();

В Nette очень легко откликаться на выполнение или невыполнение условия на стороне JavaScript методом toggle(), см. Динамический JavaScript.

Ссылка на другой элемент

Аргументом правила или условия может быть и другой элемент формы. Правило тогда будет использовать значение, которое пользователь позже введёт в браузере. Это можно использовать, например, для динамической проверки того, что элемент password содержит ту же строку, что и элемент password_confirm:

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

Собственные правила и условия

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

Методам addRule() и addCondition() можно передать первым параметром любой callback. 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;
}

private 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();

Проверку на стороне клиента можно полностью отключить, добавив форме атрибут novalidate. Скрипт netteForms.js тогда пропустит её проверку при отправке, и проверка произойдёт только на сервере:

$form->setHtmlAttribute('novalidate');

Динамический JavaScript

Хотите показывать поля адреса, только если пользователь выберет доставку товара почтой? Не проблема. Ключ к этому – пара методов addCondition() и toggle():

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

Этот код говорит, что при выполнении условия (то есть когда флажок отмечен) HTML-элемент #address-container будет виден, и наоборот. Значит, элементы формы с адресом получателя мы поместим в контейнер с этим идентификатором, и они будут скрываться или показываться по щелчку на флажке. Об этом заботится скрипт netteForms.js.

Аргументом метода toggle() может быть любой селектор. По историческим причинам строка, начинающаяся с буквы, цифры или подчёркивания и содержащая только буквы, цифры, подчёркивания, дефисы, точки и двоеточия, считается идентификатором элемента, как если бы перед ней стоял символ #. Второй необязательный параметр позволяет обратить поведение: например, если бы мы использовали toggle('#address-container', false), элемент отображался бы, только если флажок не отмечен.

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

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

Отключение проверки

Иногда проверку бывает полезно отключить. Если нажатие кнопки отправки не должно выполнять проверку (подходит для кнопок Отмена или Предпросмотр), мы отключаем её методом $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 у контейнера будет вызвано, только если этот контейнер помечен для частичной проверки.

Частичная проверка влияет и на значения, возвращаемые методом getValues(): результат содержит только значения элементов, попадающих в область проверки. Значения элементов вне этой области опускаются.

версия: 4.x