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