Validarea formularelor

Elemente obligatorii

Elementele obligatorii le marcăm cu metoda setRequired(), al cărei argument este textul mesajului de eroare, care se afișează dacă utilizatorul nu completează elementul. Dacă nu specificăm argumentul, se va utiliza mesajul de eroare implicit.

$form->addText('name', 'Nume:')
	->setRequired('Vă rugăm să introduceți numele');

Reguli

Regulile de validare le adăugăm elementelor cu metoda addRule(). Primul parametru este regula, al doilea este textul mesajului de eroare și al treilea este argumentul regulii de validare.

$form->addPassword('password', 'Parolă:')
	->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8);

Regulile de validare se verifică doar în cazul în care utilizatorul a completat elementul.

Nette vine cu o serie întreagă de reguli predefinite, ale căror nume sunt constante ale clasei Nette\Forms\Form. Pentru toate elementele putem folosi aceste reguli:

constantă descriere tip argument
Required element obligatoriu, alias pentru setRequired()
Filled element obligatoriu, alias pentru setRequired()
Blank elementul nu trebuie completat
Equal valoarea este egală cu parametrul mixed
NotEqual valoarea nu este egală cu parametrul mixed
IsIn valoarea este egală cu unul dintre elementele din array array
IsNotIn valoarea nu este egală cu niciun element din array array
Valid elementul este completat corect? (pentru condiții)

Intrări de text

Pentru elementele addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() se pot utiliza și unele dintre următoarele reguli:

MinLength lungimea minimă a textului int
MaxLength lungimea maximă a textului int
Length lungimea în interval sau lungimea exactă pereche [int, int] sau int
Email adresă de e-mail validă
URL URL absolut
Pattern se potrivește expresiei regulate string
PatternInsensitive ca Pattern, dar independent de majuscule/minuscule string
Integer valoare întreagă
Numeric alias pentru Integer
Float număr
Min valoarea minimă a elementului numeric int|float
Max valoarea maximă a elementului numeric int|float
Range valoarea în interval pereche [int|float, int|float]

Regulile de validare Integer, Numeric și Float convertesc direct valoarea la integer, respectiv float. Mai mult, regula URL acceptă și o adresă fără schemă (de ex. nette.org) și completează schema (https://nette.org). Expresia din Pattern și PatternIcase trebuie să fie valabilă pentru întreaga valoare, adică ca și cum ar fi încadrată de caracterele ^ și $.

Numărul de elemente

Pentru elementele addMultiUpload(), addCheckboxList(), addMultiSelect() se pot utiliza și următoarele reguli pentru a limita numărul de elemente selectate, respectiv fișiere încărcate:

MinLength număr minim int
MaxLength număr maxim int
Length numărul în interval sau numărul exact pereche [int, int] sau int

Încărcarea fișierelor

Pentru elementele addUpload(), addMultiUpload() se pot utiliza și următoarele reguli:

MaxFileSize dimensiunea maximă a fișierului în octeți int
MimeType tip MIME, permise caractere wildcard ('video/*') string|string[]
Image imagine JPEG, PNG, GIF, WebP, AVIF
Pattern numele fișierului se potrivește expresiei regulate string
PatternInsensitive ca Pattern, dar independent de majuscule/minuscule string

MimeType și Image necesită extensia PHP fileinfo. Faptul că fișierul sau imaginea este de tipul dorit este detectat pe baza semnăturii sale și nu verifică integritatea întregului fișier. Dacă imaginea nu este deteriorată, se poate afla, de exemplu, încercând să o încărcați.

Mesaje de eroare

Toate regulile predefinite, cu excepția Pattern și PatternInsensitive, au un mesaj de eroare implicit, deci acesta poate fi omis. Cu toate acestea, specificarea și formularea tuturor mesajelor personalizate va face formularul mai prietenos pentru utilizator.

Puteți modifica mesajele implicite în configurație, editând textele din array-ul Nette\Forms\Validator::$messages sau folosind un translator.

În textul mesajelor de eroare se pot utiliza următorii substituenți:

%d înlocuiește succesiv cu argumentele regulii
%n$d înlocuiește cu al n-lea argument al regulii
%label înlocuiește cu eticheta elementului (fără două puncte)
%name înlocuiește cu numele elementului (de ex. name)
%value înlocuiește cu valoarea introdusă de utilizator
$form->addText('name', 'Nume:')
	->setRequired('Vă rugăm să completați %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'cel puțin %d și cel mult %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'cel mult %2$d și cel puțin %1$d', [5, 10]);

Condiții

Pe lângă reguli, se pot adăuga și condiții. Acestea se scriu similar cu regulile, doar că în loc de addRule() folosim metoda addCondition() și, desigur, nu specificăm niciun mesaj de eroare (condiția doar întreabă):

$form->addPassword('password', 'Parolă:')
	// dacă parola nu este mai lungă de 8 caractere
	->addCondition($form::MaxLength, 8)
		// atunci trebuie să conțină o cifră
		->addRule($form::Pattern, 'Trebuie să conțină o cifră', '.*[0-9].*');

Condiția poate fi legată și de alt element decât cel curent folosind addConditionOn(). Ca prim parametru, specificăm referința la element. În acest exemplu, e-mailul va fi obligatoriu doar dacă se bifează checkbox-ul (valoarea sa va fi true):

$form->addCheckbox('newsletters', 'trimiteți-mi newslettere');

$form->addEmail('email', 'E-mail:')
	// dacă checkbox-ul este bifat
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// atunci solicită e-mail
		->setRequired('Introduceți adresa de e-mail');

Din condiții se pot crea structuri complexe folosind elseCondition() și endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // dacă prima condiție este îndeplinită
		->addConditionOn(/* ... */) // și a doua condiție pe un alt element
			->addRule(/* ... */) // solicită această regulă
		->elseCondition() // dacă a doua condiție nu este îndeplinită
			->addRule(/* ... */) // solicită aceste reguli
			->addRule(/* ... */)
		->endCondition() // ne întoarcem la prima condiție
		->addRule(/* ... */);

În Nette se poate reacționa foarte ușor la îndeplinirea sau neîndeplinirea condiției și pe partea de JavaScript folosind metoda toggle(), vezi javascript-dinamic.

Referință la alt element

Ca argument al regulii sau condiției se poate transmite și alt element al formularului. Regula va folosi atunci valoarea introdusă ulterior de utilizator în browser. Astfel se poate valida dinamic, de exemplu, că elementul password conține același șir ca elementul password_confirm:

$form->addPassword('password', 'Parolă');
$form->addPassword('password_confirm', 'Confirmați parola')
    ->addRule($form::Equal, 'Parolele introduse nu se potrivesc', $form['password']);

Reguli și condiții personalizate

Uneori ajungem în situația în care regulile de validare încorporate în Nette nu sunt suficiente și trebuie să validăm datele de la utilizator în felul nostru. În Nette este foarte simplu!

Metodelor addRule() sau addCondition() li se poate transmite ca prim parametru orice callback. Acesta primește ca prim parametru elementul însuși și returnează o valoare booleană care indică dacă validarea a avut loc cu succes. La adăugarea unei reguli folosind addRule(), se pot specifica și alte argumente, acestea fiind apoi transmise ca al doilea parametru.

Putem crea astfel propriul set de validatori ca o clasă cu metode statice:

class MyValidators
{
	// testează dacă valoarea este divizibilă cu argumentul
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// alți validatori
	}
}

Utilizarea este apoi foarte simplă:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'Valoarea trebuie să fie un multiplu al numărului %d',
		8,
	);

Regulile de validare personalizate pot fi adăugate și în JavaScript. Condiția este ca regula să fie o metodă statică. Numele său pentru validatorul JavaScript se formează prin concatenarea numelui clasei fără backslash-uri \, a unui underscore _ și a numelui metodei. De ex. App\MyValidators::validateDivisibility se scrie ca AppMyValidators_validateDivisibility și se adaugă la obiectul Nette.validators:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Evenimentul onValidate

După trimiterea formularului, se efectuează validarea, în timpul căreia se verifică regulile individuale adăugate folosind addRule() și apoi se declanșează evenimentul onValidate. Handler-ul său poate fi utilizat pentru validare suplimentară, de obicei verificarea combinației corecte de valori în mai multe elemente ale formularului.

Dacă se descoperă o eroare, o transmitem formularului prin metoda addError(). Aceasta poate fi apelată fie pe un element specific, fie direct pe formular.

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('Această combinație nu este posibilă.');
	}
}

Erori în timpul procesării

În multe cazuri, aflăm despre eroare abia în momentul în care procesăm formularul valid, de exemplu, scriem un nou element în baza de date și întâlnim o duplicitate a cheilor. În acest caz, transmitem din nou eroarea formularului prin metoda addError(). Aceasta poate fi apelată fie pe un element specific, fie direct pe formular:

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('Parolă invalidă.');
	}
}

Dacă este posibil, recomandăm atașarea erorii direct la elementul formularului, deoarece aceasta va fi afișată lângă el la utilizarea renderer-ului implicit.

$form['date']->addError('Ne pare rău, dar această dată este deja ocupată.');

Puteți apela addError() în mod repetat pentru a transmite formularului sau elementului mai multe mesaje de eroare. Le puteți obține folosind getErrors().

Atenție, $form->getErrors() returnează un sumar al tuturor mesajelor de eroare, inclusiv cele transmise direct elementelor individuale, nu doar direct formularului. Mesajele de eroare transmise doar formularului le puteți obține prin $form->getOwnErrors().

Modificarea intrării

Folosind metoda addFilter(), putem modifica valoarea introdusă de utilizator. În acest exemplu, vom tolera și elimina spațiile din codul poștal:

$form->addText('zip', 'Cod poștal:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // eliminăm spațiile din codul poștal
	})
	->addRule($form::Pattern, 'Codul poștal nu este în format de cinci cifre', '\d{5}');

Filtrul se integrează între regulile și condițiile de validare, deci ordinea metodelor contează, adică filtrul și regula se apelează în ordinea în care sunt metodele addFilter() și addRule().

Validare JavaScript

Limbajul pentru formularea condițiilor și regulilor este foarte puternic. Toate construcțiile funcționează atât pe partea de server, cât și pe partea de JavaScript. Acestea sunt transmise în atributele HTML data-nette-rules ca JSON. Validarea propriu-zisă este apoi efectuată de un script care interceptează evenimentul submit al formularului, parcurge elementele individuale și efectuează validarea corespunzătoare.

Acest script este netteForms.js și este disponibil din mai multe surse posibile:

Puteți include scriptul direct în pagina HTML de pe CDN:

<script src="https://unpkg.com/nette-forms@3"></script>

Sau copiați-l local în folderul public al proiectului (de ex. din vendor/nette/forms/src/assets/netteForms.min.js):

<script src="/path/to/netteForms.min.js"></script>

Sau instalați-l prin npm:

npm install nette-forms

Și apoi încărcați-l și rulați-l:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternativ, îl puteți încărca direct din folderul vendor:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

JavaScript dinamic

Doriți să afișați câmpurile pentru introducerea adresei doar dacă utilizatorul alege să primească produsul prin poștă? Nicio problemă. Cheia este perechea de metode addCondition() & toggle():

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

Acest cod spune că atunci când condiția este îndeplinită, adică atunci când checkbox-ul este bifat, elementul HTML #address-container va fi vizibil. Și invers. Elementele formularului cu adresa destinatarului le vom plasa astfel într-un container cu acest ID, iar la clic pe checkbox, acestea se vor ascunde sau afișa. Acest lucru este asigurat de scriptul netteForms.js.

Ca argument al metodei toggle() se poate transmite orice selector. Din motive istorice, un șir alfanumeric fără alte caractere speciale este înțeles ca ID-ul elementului, adică la fel ca și cum ar fi precedat de caracterul #. Al doilea parametru opțional permite inversarea comportamentului, adică dacă am folosi toggle('#address-container', false), elementul s-ar afișa doar dacă checkbox-ul nu ar fi bifat.

Implementarea implicită în JavaScript modifică proprietatea hidden a elementelor. Putem însă schimba ușor comportamentul, de exemplu, adăugând o animație. Este suficient să suprascriem în JavaScript metoda Nette.toggle cu propria soluție:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// ascundem sau afișăm 'el' în funcție de valoarea 'visible'
	});
};

Dezactivarea validării

Uneori poate fi util să dezactivăm validarea. Dacă apăsarea butonului de trimitere nu trebuie să efectueze validarea (potrivit pentru butoanele Cancel sau Preview), o dezactivăm cu metoda $submit->setValidationScope([]). Dacă trebuie să efectueze doar o validare parțială, putem specifica ce câmpuri sau containere de formular trebuie validate.

$form->addText('name')
	->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')
	->setRequired('age');
$details->addInteger('age2')
	->setRequired('age2');

$form->addSubmit('send1'); // Validează întregul formular
$form->addSubmit('send2')
	->setValidationScope([]); // Nu validează deloc
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // Validează doar elementul name
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Validează doar elementul age
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Validează containerul details

setValidationScope nu afectează evenimentul onValidate al formularului, care va fi apelat întotdeauna. Evenimentul onValidate al containerului va fi declanșat doar dacă acest container este marcat pentru validare parțială.

versiune: 4.0