Валидация на форми
Задължителни елементи
Задължителните елементи маркираме с метода 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()
. Като първи параметър посочваме референция
към елемента. В този пример имейлът ще бъде задължителен само тогава,
когато се отметне чекбоксът (неговата стойност ще бъде 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.
Референция към друг елемент
Като аргумент на правило или условие може да се предаде и друг
елемент на формата. Правилото тогава ще използва стойността, въведена
по-късно от потребителя в браузъра. Така може например динамично да се
валидира, че елементът 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
.
Неговият handler може да се използва за допълнителна валидация, типично
проверка на правилната комбинация от стойности в няколко елемента на
формата.
Ако се открие грешка, я предаваме на формата с метода 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
на
контейнера ще бъде извикано само ако този контейнер е маркиран за
частична валидация.