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.