Formularele din Prezentări

Nette Forms facilitează în mod dramatic crearea și procesarea formularelor web. În acest capitol, veți învăța cum să utilizați formularele în cadrul programelor de prezentare.

Dacă vă interesează să le folosiți complet independent, fără restul cadrului, există un ghid pentru formulare independente.

Primul formular

Vom încerca să scriem un formular de înregistrare simplu. Codul acestuia va arăta astfel:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];

iar în browser, rezultatul ar trebui să arate astfel:

Formularul din prezentator este un obiect din clasa Nette\Application\UI\Form, predecesorul său, Nette\Forms\Form, este destinat utilizării de sine stătătoare. I-am adăugat câmpurile nume, parolă și butonul de trimitere. În cele din urmă, linia cu $form->onSuccess spune că, după trimiterea și validarea cu succes, trebuie apelată metoda $this->formSucceeded().

Din punctul de vedere al prezentatorului, formularul este o componentă comună. Prin urmare, acesta este tratat ca o componentă și încorporat în prezentator folosind metoda factory. Acesta va arăta astfel:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// aici vom procesa datele trimise de formular
		// $data->name conține nume
		// $data->password conține parola
		$this->flashMessage('You have successfully signed up.');
		$this->redirect('Home:');
	}
}

Iar redarea în șablon se face cu ajutorul etichetei {control}:

<h1>Registration</h1>

{control registrationForm}

Și asta e tot :-) Avem un formular funcțional și perfect securizat.

Acum probabil că vă gândiți că a fost prea rapid, întrebându-vă cum este posibil ca metoda formSucceeded() să fie apelată și ce parametri primește. Sigur, aveți dreptate, acest lucru merită o explicație.

Nette vine cu un mecanism interesant, pe care îl numim stilul Hollywood. În loc să trebuiască să întrebați constant dacă s-a întâmplat ceva (“a fost trimis formularul?”, “a fost trimis în mod valid?” sau “nu a fost falsificat?”), îi spuneți framework-ului “când formularul este completat în mod valid, apelați această metodă” și lăsați să lucreze mai departe la ea. Dacă programați în JavaScript, sunteți familiarizat cu acest stil de programare. Scrieți funcții care sunt apelate atunci când apare un anumit eveniment. Iar limbajul le trece argumentele corespunzătoare.

Acesta este modul în care este construit codul prezentatorului de mai sus. Array $form->onSuccess reprezintă lista de callback-uri PHP pe care Nette le va apela atunci când formularul este trimis și completat corect. În cadrul ciclului de viață al prezentatorului este un așa-numit semnal, astfel încât acestea sunt apelate după metoda action* și înainte de metoda render*. Și transmite la fiecare callback formularul propriu-zis în primul parametru și datele trimise ca obiect ArrayHash în al doilea. Puteți omite primul parametru dacă nu aveți nevoie de obiectul formular. Al doilea parametru poate fi chiar mai util, dar despre asta vom vorbi mai târziu.

Obiectul $data conține proprietățile name și password cu datele introduse de utilizator. De obicei, trimitem datele direct pentru o prelucrare ulterioară, care poate fi, de exemplu, inserarea în baza de date. Cu toate acestea, poate apărea o eroare în timpul procesării, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi la formular folosind addError() și îl lăsăm să se redeseneze, cu un mesaj de eroare:

$form->addError('Sorry, username is already in use.');

În plus față de onSuccess, există și onSubmit: callback-urile sunt întotdeauna apelate după ce formularul este trimis, chiar dacă nu este completat corect. Și, în sfârșit, onError: callback-urile sunt apelate numai dacă trimiterea nu este validă. Ele sunt apelate chiar dacă invalidăm formularul în onSuccess sau onSubmit folosind addError().

După procesarea formularului, vom redirecționa către pagina următoare. Acest lucru împiedică retrimiterea neintenționată a formularului prin apăsarea butonului refresh, back sau prin mutarea istoricului browserului.

Încercați să adăugați mai multe controale de formular.

Accesul la controale

Formularul este o componentă a dispozitivului de prezentare, în cazul nostru numit registrationForm (după numele metodei de fabrică createComponentRegistrationForm), astfel încât oriunde în dispozitiv se poate ajunge la formular folosind:

$form = $this->getComponent('registrationForm');
// sintaxa alternativă: $form = $this['registrationForm'];

De asemenea, controalele individuale ale formularului sunt componente, deci le puteți accesa în același mod:

$input = $form->getComponent('name'); // sau $input = $form['name'];
$button = $form->getComponent('send'); // sau $button = $form['send'];

Controalele sunt eliminate cu ajutorul funcției unset:

unset($form['name']);

Reguli de validare

Cuvântul valid a fost folosit de mai multe ori, dar formularul nu are încă reguli de validare. Să rezolvăm problema.

Numele va fi obligatoriu, așa că îl vom marca cu metoda setRequired(), al cărei argument este textul mesajului de eroare care va fi afișat în cazul în care utilizatorul nu îl completează. Dacă nu se dă niciun argument, se folosește mesajul de eroare implicit.

$form->addText('name', 'Name:')
	->setRequired('Please fill your name.');

Încercați să trimiteți formularul fără ca numele să fie completat și veți vedea că se afișează un mesaj de eroare, iar browserul sau serverul îl va respinge până când îl veți completa.

În același timp, nu veți putea păcăli sistemul introducând doar spații în câmp, de exemplu. În niciun caz. Nette taie automat spațiile albe din stânga și din dreapta. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna la fiecare intrare pe o singură linie, dar este adesea uitat. Nette o face automat. (Puteți încerca să păcăliți formularele și să trimiteți un șir de caractere multiliniar ca nume. Chiar și în acest caz, Nette nu se va lăsa păcălit și întreruperile de linie se vor schimba în spații).

Formularul este întotdeauna validat pe partea serverului, dar este generată și validarea JavaScript, care este rapidă, iar utilizatorul știe imediat de eroare, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul netteForms.js. Introduceți-l în șablonul de prezentare:

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

Dacă vă uitați în codul sursă al paginii cu formular, puteți observa că Nette inserează câmpurile obligatorii în elemente cu o clasă CSS required. Încercați să adăugați următorul stil în șablon, iar eticheta “Name” va fi roșie. În mod elegant, marcăm câmpurile obligatorii pentru utilizatori:

<style>
.required label { color: maroon }
</style>

Reguli de validare suplimentare vor fi adăugate prin metoda addRule(). Primul parametru este regula, al doilea este din nou textul mesajului de eroare, iar argumentul opțional al regulii de validare poate urma. Ce înseamnă acest lucru?

Formularul va primi o altă intrare opțională age cu condiția ca aceasta să fie un număr (addInteger()) și să se încadreze în anumite limite ($form::Range). Și aici vom folosi al treilea argument din addRule(), intervalul propriu-zis:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);

În cazul în care utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece câmpul este opțional.

Evident, există loc pentru o mică refactorizare. În mesajul de eroare și în cel de-al treilea parametru, numerele sunt listate în dublu exemplar, ceea ce nu este ideal. Dacă am crea un formular multilingv, iar mesajul care conține numere ar trebui tradus în mai multe limbi, ar îngreuna modificarea valorilor. Din acest motiv, se pot utiliza caracterele de substituție %d:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Să ne întoarcem la câmpul parolă, să îl facem obligatoriu și să verificăm lungimea minimă a parolei ($form::MinLength), folosind din nou caracterele de substituție din mesaj:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);

Vom adăuga un câmp passwordVerify la formular, în care utilizatorul introduce din nou parola, pentru verificare. Folosind reguli de validare, vom verifica dacă ambele parole sunt identice ($form::Equal). Iar ca argument vom da o referință la prima parolă folosind paranteze pătrate:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();

Folosind setOmitted(), marcăm un element a cărui valoare nu ne interesează cu adevărat și care există doar pentru validare. Valoarea sa nu este transmisă la $data.

Avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai largi, puteți crea condiții, afișa și ascunde părți ale unei pagini în funcție de acestea etc. Puteți afla totul în capitolul dedicat validării formularelor.

Valori implicite

Adesea se stabilesc valori implicite pentru controalele de formular:

$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);

Adesea este util să setați valorile implicite pentru toate controalele deodată. De exemplu, atunci când formularul este utilizat pentru a edita înregistrări. Citim înregistrarea din baza de date și o setăm ca valoare implicită:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Apelați setDefaults() după definirea controalelor.

Redarea formularului

În mod implicit, formularul este redat sub forma unui tabel. Controalele individuale respectă orientările de bază privind accesibilitatea web. Toate etichetele sunt generate ca <label> elemente și sunt asociate cu intrările lor, făcând clic pe etichetă se mută cursorul pe intrare.

Putem seta orice atribute HTML pentru fiecare element. De exemplu, adăugați un marcaj de loc:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

Există într-adevăr o mulțime de moduri de redare a unui formular, așa că este un capitol dedicat redării.

Maparea în clase

Să ne întoarcem la metoda formSucceeded(), care, în al doilea parametru $data, primește datele trimise sub forma unui obiect ArrayHash. Deoarece aceasta este o clasă generică, ceva de genul stdClass, ne vor lipsi unele facilități atunci când lucrăm cu ea, cum ar fi completarea codului pentru proprietăți în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat prin existența unei clase specifice pentru fiecare formular, ale cărei proprietăți să reprezinte controalele individuale. De ex:

class RegistrationFormData
{
	public string $name;
	public int $age;
	public string $password;
}

Începând cu PHP 8.0, puteți utiliza această notație elegantă care folosește un constructor:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public int $age,
		public string $password,
	) {
	}
}

Cum să îi spuneți lui Nette să returneze datele ca obiecte din această clasă? Mai ușor decât credeți. Tot ceea ce trebuie să faceți este să specificați clasa ca tip al parametrului $data în handler:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name este o instanță de RegistrationFormData
	$name = $data->name;
	// ...
}

De asemenea, puteți specifica array ca tip și atunci datele vor fi transmise ca o matrice.

În mod similar, puteți utiliza metoda getValues(), pe care o trecem ca nume de clasă sau obiect de hidratat ca parametru:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

În cazul în care formularele constau într-o structură pe mai multe niveluri compusă din containere, creați o clasă separată pentru fiecare dintre ele:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public int $age;
	public string $password;
}

În acest caz, cartografierea știe din tipul de proprietate $person că trebuie să mapeze containerul la clasa PersonFormData. În cazul în care proprietatea ar trebui să conțină o matrice de containere, furnizați tipul array și treceți clasa care urmează să fie mapată direct la container:

$person->setMappedType(PersonFormData::class);

Puteți genera o propunere pentru clasa de date a unui formular utilizând metoda Nette\Forms\Blueprint::dataClass($form), care o va imprima în pagina browserului. Puteți apoi să dați un simplu clic pentru a selecta și a copia codul în proiectul dumneavoastră.

Butoane de trimitere multiple

În cazul în care formularul are mai multe butoane, de obicei trebuie să distingem care dintre ele a fost apăsat. Putem crea o funcție proprie pentru fiecare buton. Setați-o ca gestionar pentru evenimentul onClick:

$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

Acești gestionari sunt, de asemenea, apelați numai în cazul în care formularul este valid, ca în cazul evenimentului onSuccess. Diferența constă în faptul că primul parametru poate fi obiectul butonului de trimitere în loc de formular, în funcție de tipul pe care îl specificați:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Atunci când un formular este trimis cu tasta Enter, acesta este tratat ca și cum ar fi fost trimis cu primul buton.

Eveniment onAnchor

Atunci când construiți un formular printr-o metodă factory (cum ar fi createComponentRegistrationForm), acesta nu știe încă dacă a fost trimis sau datele cu care a fost trimis. Dar există cazuri în care trebuie să cunoaștem valorile trimise, poate că de ele depinde cum va arăta formularul, sau sunt folosite pentru casetele de selectare dependente etc.

Prin urmare, puteți face ca codul care construiește formularul să fie apelat atunci când acesta este ancorat, adică este deja legat de prezentator și cunoaște datele trimise. Vom plasa un astfel de cod în matricea $onAnchor:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// această funcție va fi apelată atunci când formularul cunoaște datele cu care a fost trimis.
	// astfel încât puteți utiliza metoda getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val): []);
};

Protecția împotriva vulnerabilităților

Nette Framework depune un mare efort pentru a fi sigur și, deoarece formularele sunt cele mai frecvente intrări ale utilizatorului, formularele Nette sunt ca și impenetrabile. Totul este menținut în mod dinamic și transparent, nimic nu trebuie setat manual.

Pe lângă protejarea formularelor împotriva atacurilor care vizează vulnerabilități bine cunoscute, cum ar fi Cross-Site Scripting (XSS ) și Cross-Site Request Forgery (CSRF), face o mulțime de mici sarcini de securitate la care nu mai trebuie să vă gândiți.

De exemplu, filtrează toate caracterele de control din intrări și verifică validitatea codificării UTF-8, astfel încât datele din formular vor fi întotdeauna curate. Pentru căsuțele de selectare și listele radio, verifică dacă elementele selectate sunt într-adevăr dintre cele oferite și dacă nu a existat nicio falsificare. Am menționat deja că, pentru introducerea textului pe o singură linie, elimină caracterele de sfârșit de linie pe care un atacator le-ar putea trimite acolo. În cazul intrărilor de text pe mai multe linii, normalizează caracterele de sfârșit de linie. Și așa mai departe.

Nette rezolvă pentru dumneavoastră vulnerabilitățile de securitate despre care majoritatea programatorilor habar nu au că există.

Atacul CSRF menționat constă în faptul că un atacator atrage victima să viziteze o pagină care execută în tăcere o cerere în browserul victimei către serverul unde victima este conectată în acel moment, iar serverul crede că cererea a fost făcută de către victimă în mod voit. Prin urmare, Nette împiedică trimiterea formularului prin POST de pe un alt domeniu. Dacă dintr-un motiv oarecare doriți să dezactivați protecția și să permiteți ca formularul să fie trimis de pe un alt domeniu, utilizați:

$form->allowCrossOrigin(); // ATENȚIE! Dezactivează protecția!

Această protecție utilizează un cookie SameSite numit _nss. Este posibil ca protecția prin cookie SameSite să nu fie 100% fiabilă, așa că este o idee bună să activați protecția prin token:

$form->addProtection();

Este foarte recomandat să aplicați această protecție formularelor dintr-o parte administrativă a aplicației dumneavoastră care modifică date sensibile. Cadrul protejează împotriva unui atac CSRF prin generarea și validarea token-ului de autentificare care este stocat într-o sesiune (argumentul este mesajul de eroare afișat în cazul în care token-ul a expirat). De aceea, este necesar să aveți o sesiune pornită înainte de a afișa formularul. În partea de administrare a site-ului web, de obicei, sesiunea este deja începută, datorită autentificării utilizatorului. În caz contrar, începeți sesiunea cu metoda Nette\Http\Session::start().

Utilizarea unui formular în mai multe prezentări

Dacă aveți nevoie să utilizați un formular în mai multe prezentări, vă recomandăm să creați o fabrică pentru acesta, pe care apoi să o transmiteți prezentatorului. O locație potrivită pentru o astfel de clasă este, de exemplu, directorul app/Forms.

Clasa factory ar putea arăta astfel:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}

Cerem clasei să producă formularul în metoda factory pentru componentele din prezentator:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// putem modifica formularul, aici de exemplu schimbăm eticheta de pe buton
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // și adăugăm handlerul
	return $form;
}

Gestionarul de procesare a formularului poate fi, de asemenea, livrat din fabrică:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		$form->onSuccess[] = function (Form $form, $data): void {
			// procesăm formularul nostru trimis aici
		};
		return $form;
	}
}

Așadar, avem o scurtă introducere în formulare în Nette. Încercați să căutați în directorul de exemple din distribuție pentru mai multă inspirație.

versiune: 4.0