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