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

Обов'язкові елементи

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

У тексті повідомлень про помилки можна використовувати ці рядки-заповнювачі:

%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(). Як перший параметр вкажемо посилання на елемент. У цьому прикладі e-mail буде обов'язковим лише тоді, коли буде відмічено checkbox (його значення буде true):

$form->addCheckbox('newsletters', 'надсилайте мені розсилки');

$form->addEmail('email', 'E-mail:')
	// якщо checkbox відмічено
	->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');

Цей код говорить, що коли умова виконана, тобто коли відмічено checkbox, буде видимим HTML-елемент #address-container. І навпаки. Елементи форми з адресою одержувача ми розмістимо в контейнері з цим ID, і при кліку на checkbox вони будуть приховані або показані. Це забезпечує скрипт netteForms.js.

Як аргумент методу toggle() можна передати будь-який селектор. З історичних причин буквено-цифровий рядок без інших спеціальних символів розуміється як ID елемента, тобто так само, якби йому передував символ #. Другий необов'язковий параметр дозволяє інвертувати поведінку, тобто якби ми використали toggle('#address-container', false), елемент би, навпаки, відображався лише тоді, коли checkbox не був би відмічений.

Стандартна реалізація в 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