Formulare în presentere

Nette Forms facilitează enorm crearea și procesarea formularelor web. În acest capitol, veți învăța cum să utilizați formularele în interiorul presenterelor.

Dacă sunteți interesat de cum să le utilizați complet independent, fără restul framework-ului, ghidul pentru utilizare independentă este pentru dumneavoastră.

Primul formular

Să încercăm să scriem un formular simplu de înregistrare. Codul său va fi următorul:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Nume:');
$form->addPassword('password', 'Parolă:');
$form->addSubmit('send', 'Înregistrează-te');
$form->onSuccess[] = [$this, 'formSucceeded'];

și în browser se va afișa astfel:

Formularul în presenter este un obiect al clasei Nette\Application\UI\Form, predecesorul său Nette\Forms\Form este destinat utilizării independente. Am adăugat în el elementele numite nume, parolă și un buton de trimitere. Și în final, linia cu $form->onSuccess spune că după trimitere și validare reușită, trebuie apelată metoda $this->formSucceeded().

Din perspectiva presenterului, formularul este o componentă obișnuită. Prin urmare, este tratat ca o componentă și îl vom integra în presenter folosind metode factory. 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', 'Nume:');
		$form->addPassword('password', 'Parolă:');
		$form->addSubmit('send', 'Înregistrează-te');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// aici procesăm datele trimise de formular
		// $data->name conține numele
		// $data->password conține parola
		$this->flashMessage('Ați fost înregistrat cu succes.');
		$this->redirect('Home:');
	}
}

Și în șablon, randăm formularul cu tag-ul {control}:

<h1>Înregistrare</h1>

{control registrationForm}

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

Și acum probabil vă gândiți că a fost prea rapid, vă întrebați cum este posibil să fie apelată metoda formSucceeded() și care sunt parametrii pe care îi primește. Sigur, aveți dreptate, acest lucru merită o explicație.

Nette vine cu un mecanism proaspăt, pe care îl numim Stil Hollywood. În loc ca dumneavoastră, ca dezvoltator, să trebuiască să întrebați constant dacă s-a întâmplat ceva („a fost trimis formularul?”, „a fost trimis valid?” și „nu a fost falsificat?”), spuneți framework-ului „când formularul este completat valid, apelează această metodă” și lăsați restul muncii pe seama lui. Dacă programați în JavaScript, acest stil de programare vă este familiar. Scrieți funcții care sunt apelate atunci când are loc un anumit eveniment. Și limbajul le transmite argumentele corespunzătoare.

Exact așa este construit și codul presenterului de mai sus. Array-ul $form->onSuccess reprezintă o listă de callback-uri PHP, pe care Nette le apelează în momentul în care formularul este trimis și completat corect (adică este valid). În cadrul ciclului de viață al presenterului, este vorba despre un așa-numit semnal, deci sunt apelate după metoda action* și înainte de metoda render*. Și fiecărui callback îi transmite ca prim parametru formularul însuși și ca al doilea, datele trimise sub forma unui obiect ArrayHash. Primul parametru poate fi omis dacă nu aveți nevoie de obiectul formularului. Iar al doilea parametru poate fi mai inteligent, dar despre asta mai târziu.

Obiectul $data conține proprietățile name și password cu datele completate de utilizator. De obicei, trimitem datele direct pentru procesare ulterioară, ceea ce poate fi, de exemplu, inserarea în baza de date. În timpul procesării, însă, poate apărea o eroare, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi în formular folosind addError() și îl lăsăm să fie randat din nou, inclusiv cu mesajul de eroare.

$form->addError('Ne pare rău, numele de utilizator este deja folosit de altcineva.');

Pe lângă onSuccess, mai există și onSubmit: callback-urile sunt apelate întotdeauna după trimiterea formularului, chiar și atunci când nu este completat corect. Și, de asemenea, onError: callback-urile sunt apelate doar dacă trimiterea nu este validă. Sunt apelate chiar și atunci când în onSuccess sau onSubmit invalidăm formularul folosind addError().

După procesarea formularului, redirecționăm către pagina următoare. Acest lucru previne retrimiterea nedorită a formularului prin butonul reîmprospătare, înapoi sau prin navigarea în istoricul browserului.

Încercați să adăugați și alte elemente de formular.

Accesul la elemente

Formularul este o componentă a presenterului, în cazul nostru numită registrationForm (după numele metodei factory createComponentRegistrationForm), așa că oriunde în presenter puteți accesa formularul folosind:

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

Componentele sunt și elementele individuale ale formularului, de aceea le puteți accesa în același mod:

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

Elementele se elimină folosind unset:

unset($form['name']);

Reguli de validare

S-a menționat cuvântul valid, dar formularul nu are încă nicio regulă de validare. Să remediem acest lucru.

Numele va fi obligatoriu, de aceea îl marcăm cu metoda setRequired(), al cărei argument este textul mesajului de eroare care se afișează dacă utilizatorul nu completează numele. Dacă nu specificăm argumentul, se va folosi mesajul de eroare implicit.

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

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

În același timp, nu puteți păcăli sistemul scriind, de exemplu, doar spații în câmp. Nici vorbă. Nette elimină automat spațiile de la începutul și sfârșitul șirului. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna cu fiecare input de o singură linie, dar adesea se uită. Nette o face automat. (Puteți încerca să păcăliți formularul și să trimiteți un șir multilinie ca nume. Nici aici Nette nu se lasă păcălit și transformă sfârșiturile de rând în spații.)

Formularul este întotdeauna validat pe partea de server, dar se generează și o validare JavaScript, care se execută instantaneu, iar utilizatorul află despre eroare imediat, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul netteForms.js. Inserați-l în șablonul de layout:

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

Dacă vă uitați la codul sursă al paginii cu formularul, puteți observa că Nette inserează elementele obligatorii în elemente cu clasa CSS required. Încercați să adăugați următorul stil în șablon și eticheta „Nume” va fi roșie. Astfel, marcăm elegant elementele obligatorii pentru utilizatori:

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

Alte reguli de validare le adăugăm cu metoda addRule(). Primul parametru este regula, al doilea este din nou textul mesajului de eroare și poate urma un argument al regulii de validare. Ce înseamnă asta?

Vom extinde formularul cu un nou câmp opțional „vârstă”, care trebuie să fie un număr întreg (addInteger()) și, în plus, într-un interval permis ($form::Range). Și aici vom folosi al treilea parametru al metodei addRule(), prin care transmitem validatorului intervalul dorit ca o pereche [de la, până la]:

$form->addInteger('age', 'Vârsta:')
	->addRule($form::Range, 'Vârsta trebuie să fie între 18 și 120', [18, 120]);

Dacă utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece elementul este opțional.

Aici apare spațiu pentru o mică refactorizare. În mesajul de eroare și în al treilea parametru, numerele sunt menționate duplicat, ceea ce nu este ideal. Dacă am crea formulare multilingve și mesajul care conține numere ar fi tradus în mai multe limbi, o eventuală modificare a valorilor ar fi dificilă. Din acest motiv, este posibil să folosim substituenți %d și Nette va completa valorile:

	->addRule($form::Range, 'Vârsta trebuie să fie între %d și %d ani', [18, 120]);

Să ne întoarcem la elementul password, pe care îl vom face, de asemenea, obligatoriu și vom verifica lungimea minimă a parolei ($form::MinLength), din nou folosind substituentul:

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

Adăugăm în formular și câmpul passwordVerify, unde utilizatorul introduce parola încă o dată, pentru verificare. Folosind regulile de validare, verificăm dacă ambele parole sunt identice ($form::Equal). Și ca parametru, dăm o referință la prima parolă folosind paranteze drepte:

$form->addPassword('passwordVerify', 'Parola pentru verificare:')
	->setRequired('Vă rugăm introduceți parola încă o dată pentru verificare')
	->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password'])
	->setOmitted();

Cu setOmitted(), am marcat elementul a cărui valoare nu ne interesează de fapt și care există doar în scopul validării. Valoarea nu se transmite în $data.

Astfel, avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai largi, se pot crea condiții, se pot afișa și ascunde părți ale paginii în funcție de acestea etc. Veți afla totul în capitolul despre validarea formularelor.

Valori implicite

Elementelor formularului le setăm în mod obișnuit valori implicite:

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

Adesea este util să setăm valori implicite pentru toate elementele simultan. De exemplu, când formularul servește pentru editarea înregistrărilor. Citim înregistrarea din baza de date și setăm valorile implicite:

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

Apelați setDefaults() după definirea elementelor.

Randarea formularului

În mod standard, formularul se randează ca un tabel. Elementele individuale respectă regula de bază a accesibilității – toate etichetele sunt scrise ca <label> și legate de elementul de formular corespunzător. La clic pe etichetă, cursorul apare automat în câmpul formularului.

Fiecărui element îi putem seta atribute HTML arbitrare. De exemplu, adăugăm un placeholder:

$form->addInteger('age', 'Vârsta:')
	->setHtmlAttribute('placeholder', 'Vă rugăm să completați vârsta');

Există într-adevăr o mare varietate de moduri de a randa un formular, așa că există un capitol separat despre randare dedicat acestui subiect.

Maparea la clase

Să ne întoarcem la metoda formSucceeded(), care în al doilea parametru $data primește datele trimise ca obiect ArrayHash. Deoarece este o clasă generică, ceva de genul stdClass, ne va lipsi un anumit confort în lucrul cu ea, cum ar fi sugerarea proprietăților în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat având o clasă specifică pentru fiecare formular, ale cărei proprietăți reprezintă elementele individuale. De ex.:

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

Alternativ, puteți utiliza constructorul:

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

Proprietățile clasei de date pot fi, de asemenea, enum-uri și vor fi mapate automat.

Cum să spunem Nette să ne returneze datele ca obiecte ale acestei clase? Mai ușor decât credeți. Este suficient doar să specificați clasa ca tip al parametrului $data în metoda handler:

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

Ca tip se poate specifica și array și atunci datele vor fi transmise ca array.

În mod similar, se poate utiliza și metoda getValues(), căreia îi transmitem numele clasei sau obiectul de hidratat ca parametru:

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

Dacă formularele formează o structură multinivel compusă din containere, creați o clasă separată pentru fiecare:

$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;
}

Maparea va recunoaște apoi din tipul proprietății $person că trebuie să mapeze containerul la clasa PersonFormData. Dacă proprietatea ar conține un array de containere, specificați tipul array și transmiteți clasa pentru mapare direct containerului:

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

Puteți genera designul clasei de date a formularului folosind metoda Nette\Forms\Blueprint::dataClass($form), care o va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să-l copiați în proiect.

Mai multe butoane

Dacă formularul are mai mult de un buton, de obicei trebuie să distingem care dintre ele a fost apăsat. Putem crea o funcție handler proprie pentru fiecare buton. O setăm ca handler pentru evenimentul onClick:

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

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

Acești handleri sunt apelați doar în cazul unui formular completat valid, la fel ca în cazul evenimentului onSuccess. Diferența este că, în loc de formular, ca prim parametru se poate transmite butonul de trimitere, depinde de tipul pe care îl specificați:

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

Când formularul este trimis cu tasta Enter, se consideră ca și cum ar fi fost trimis cu primul buton.

Evenimentul onAnchor

Când construim formularul în metoda factory (cum ar fi createComponentRegistrationForm), acesta nu știe încă dacă a fost trimis, nici cu ce date. Dar există cazuri în care avem nevoie să cunoaștem valorile trimise, de exemplu, forma ulterioară a formularului depinde de ele, sau avem nevoie de ele pentru selectbox-uri dependente etc.

O parte a codului care construiește formularul poate fi, prin urmare, lăsată să fie apelată doar în momentul în care este așa-numit ancorat, adică este deja conectat la presenter și cunoaște datele sale trimise. Un astfel de cod îl transmitem în array-ul $onAnchor:

$country = $form->addSelect('country', 'Țara:', $this->model->getCountries());
$city = $form->addSelect('city', 'Oraș:');

$form->onAnchor[] = function () use ($country, $city) {
	// această funcție va fi apelată doar când formularul știe dacă a fost trimis și cu ce date
	// deci se poate folosi metoda getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Protecția împotriva vulnerabilităților

Nette Framework pune un accent deosebit pe securitate și, prin urmare, acordă o atenție deosebită securizării formularelor. Face acest lucru complet transparent și nu necesită setări manuale.

Pe lângă faptul că protejează formularele împotriva atacurilor Cross Site Scripting (XSS) și Cross-Site Request Forgery (CSRF), realizează o mulțime de mici măsuri 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. La select box-uri și liste radio, verifică dacă elementele selectate au fost într-adevăr dintre cele oferite și nu a avut loc o falsificare. Am menționat deja că la intrările text de o singură linie elimină caracterele de sfârșit de rând, pe care un atacator le-ar fi putut trimite. La intrările multilinie, normalizează caracterele de sfârșit de rând. Și așa mai departe.

Nette rezolvă pentru dumneavoastră riscurile de securitate despre care mulți programatori nici nu bănuiesc că există.

Atacul CSRF menționat constă în faptul că atacatorul atrage victima pe o pagină care execută discret în browserul victimei o cerere către serverul pe care victima este autentificată, iar serverul crede că cererea a fost executată de victimă din proprie voință. De aceea, Nette împiedică trimiterea formularului POST de pe un alt domeniu. Dacă, din anumite motive, doriți să dezactivați protecția și să permiteți trimiterea formularului de pe un alt domeniu, utilizați:

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

Această protecție utilizează un cookie SameSite numit _nss. Protecția prin cookie SameSite poate să nu fie 100% fiabilă, de aceea este recomandat să activați și protecția prin token:

$form->addProtection();

Recomandăm protejarea în acest mod a formularelor din partea de administrare a site-ului, care modifică date sensibile în aplicație. Framework-ul se apără împotriva atacului CSRF prin generarea și verificarea unui token de autorizare, care se stochează în sesiune (session). De aceea, este necesar ca sesiunea să fie deschisă înainte de afișarea formularului. În partea de administrare a site-ului, de obicei, sesiunea este deja pornită datorită autentificării utilizatorului. Altfel, porniți sesiunea cu metoda Nette\Http\Session::start().

Același formular în mai multe presentere

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

Clasa fabrică poate arăta, de exemplu, astfel:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nume:');
		$form->addSubmit('send', 'Conectează-te');
		return $form;
	}
}

Solicităm clasei să producă formularul în metoda factory pentru componente din presenter:

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

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// putem modifica formularul, aici de exemplu schimbăm eticheta butonului
	$form['send']->setCaption('Continuă');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // și adăugăm handler
	return $form;
}

Handlerul pentru procesarea formularului poate fi, de asemenea, furnizat deja din fabrică:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nume:');
		$form->addSubmit('send', 'Conectează-te');
		$form->onSuccess[] = function (Form $form, $data): void {
			// aici efectuăm procesarea formularului
		};
		return $form;
	}
}

Așadar, am parcurs o introducere rapidă în formularele din Nette. Încercați să vă uitați și în directorul examples din distribuție, unde veți găsi mai multă inspirație.

versiune: 4.0