Obrazci v presenterjih
Nette Forms bistveno olajšajo ustvarjanje in obdelavo spletnih obrazcev. V tem poglavju se boste seznanili z uporabo obrazcev znotraj presenterjev.
Če vas zanima, kako jih uporabljati popolnoma samostojno brez preostalega ogrodja, je za vas namenjen vodič za samostojno uporabo.
Prvi obrazec
Poskusimo napisati preprost registracijski obrazec. Njegova koda bo naslednja:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Ime:');
$form->addPassword('password', 'Geslo:');
$form->addSubmit('send', 'Registriraj');
$form->onSuccess[] = [$this, 'formSucceeded'];
in v brskalniku se bo prikazal takole:

Obrazec v presenterju je objekt razreda Nette\Application\UI\Form
, njegov predhodnik
Nette\Forms\Form
je namenjen samostojni uporabi. Dodali smo mu t.i. elemente ime, geslo in gumb za oddajo. In na
koncu vrstica z $form->onSuccess
pove, da se mora po oddaji in uspešni validaciji poklicati metoda
$this->formSucceeded()
.
Z vidika presenterja je obrazec običajna komponenta. Zato se z njim kot s komponento ravna in ga vključimo v presenter s pomočjo tovarne metode. Izgledalo bo takole:
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Ime:');
$form->addPassword('password', 'Geslo:');
$form->addSubmit('send', 'Registriraj');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// tukaj obdelamo podatke, poslane z obrazcem
// $data->name vsebuje ime
// $data->password vsebuje geslo
$this->flashMessage('Uspešno ste bili registrirani.');
$this->redirect('Home:');
}
}
In v predlogi obrazec izrišemo z oznako {control}
:
<h1>Registracija</h1>
{control registrationForm}
In to je pravzaprav vse :-) Imamo delujoč in popolnoma zavarovan obrazec.
In zdaj si verjetno mislite, da je bilo prehitro, razmišljate, kako je mogoče, da se pokliče metoda
formSucceeded()
in kaj so parametri, ki jih prejme. Seveda, imate prav, to si zasluži pojasnilo.
Nette namreč prihaja s svežim mehanizmom, ki mu pravimo Hollywood style. Namesto da bi se kot razvijalec morali nenehno spraševati, ali se je nekaj zgodilo („ali je bil obrazec poslan?“, „ali je bil poslan veljavno?“ in „ali ni prišlo do njegovega ponarejanja?“), poveste ogrodju „ko bo obrazec veljavno izpolnjen, pokliči to metodo“ in prepustite nadaljnje delo njemu. Če programirate v JavaScriptu, ta slog programiranja dobro poznate. Pišete funkcije, ki se kličejo, ko nastopi določen dogodek. In jezik jim preda ustrezne argumente.
Prav tako je zgrajena tudi zgoraj navedena koda presenterja. Polje $form->onSuccess
predstavlja seznam PHP
povratnih klicev (callbackov), ki jih Nette pokliče v trenutku, ko je obrazec poslan in pravilno izpolnjen (tj. je veljaven).
V okviru življenjskega cikla
presenterja gre za t.i. signal, kličejo se torej po action*
metodi in pred render*
metodo. In
vsakemu povratnemu klicu preda kot prvi parameter sam obrazec in kot drugega poslane podatke v obliki objekta ArrayHash. Prvi parameter lahko izpustite, če objekta obrazca ne
potrebujete. In drugi parameter zna biti bolj prebrisan, ampak o tem kasneje.
Objekt $data
vsebuje ključe name
in password
s podatki, ki jih je izpolnil uporabnik.
Običajno podatke takoj pošljemo k nadaljnji obdelavi, kar je lahko na primer vstavljanje v podatkovno bazo. Med obdelavo pa se
lahko pojavi napaka, na primer uporabniško ime je že zasedeno. V takem primeru napako predamo nazaj v obrazec s pomočjo
addError()
in pustimo, da se ponovno izriše, tudi s sporočilom o napaki.
$form->addError('Oprostite, uporabniško ime že nekdo uporablja.');
Poleg onSuccess
obstaja še onSubmit
: povratni klici se kličejo vedno po oddaji obrazca, tudi
takrat, ko ni pravilno izpolnjen. In dalje onError
: povratni klici se kličejo le, če oddaja ni veljavna. Pokličejo
se celo takrat, če v onSuccess
ali onSubmit
znevalidiramo obrazec s pomočjo
addError()
.
Po obdelavi obrazca preusmerimo na naslednjo stran. S tem se prepreči neželeno ponovno pošiljanje obrazca z gumbom osveži, nazaj ali premikanjem v zgodovini brskalnika.
Poskusite dodati tudi druge elemente obrazca.
Dostop do elementov
Obrazec je komponenta presenterja, v našem primeru poimenovana registrationForm
(po imenu tovarne metode
createComponentRegistrationForm
), tako da kjerkoli v presenterju do obrazca pridete s pomočjo:
$form = $this->getComponent('registrationForm');
// alternativna sintaksa: $form = $this['registrationForm'];
Komponente so tudi posamezni elementi obrazca, zato do njih pridete na enak način:
$input = $form->getComponent('name'); // ali $input = $form['name'];
$button = $form->getComponent('send'); // ali $button = $form['send'];
Elementi se odstranijo s pomočjo unset:
unset($form['name']);
Validacijska pravila
Padla je beseda veljaven, ampak obrazec zaenkrat nima nobenih validacijskih pravil. Popravimo to.
Ime bo obvezno, zato ga označimo z metodo setRequired()
, katere argument je besedilo sporočila o napaki, ki se
prikaže, če uporabnik imena ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki.
$form->addText('name', 'Ime:')
->setRequired('Prosimo, vnesite ime');
Poskusite poslati obrazec brez izpolnjenega imena in videli boste, da se prikaže sporočilo o napaki in brskalnik ali strežnik ga bo zavračal, dokler polja ne izpolnite.
Hkrati sistema ne boste prelisičili s tem, da v polje napišete na primer le presledke. Kje pa. Nette leve in desne presledke samodejno odstranjuje. Preizkusite to. To je stvar, ki bi jo morali z vsakim enovrstičnim vnosom vedno narediti, a se nanjo pogosto pozabi. Nette to dela samodejno. (Lahko poskusite prelisičiti obrazec in kot ime poslati večvrstični niz. Tudi tukaj se Nette ne pusti zmesti in prelome vrstic spremeni v presledke.)
Obrazec se vedno validira na strani strežnika, vendar se generira tudi JavaScript validacija, ki poteka bliskovito in
uporabnik se o napaki seznani takoj, brez potrebe po pošiljanju obrazca na strežnik. Za to skrbi skript
netteForms.js
. Vstavite ga v predlogo postavitve:
<script src="https://unpkg.com/nette-forms@3"></script>
Če pogledate v izvorno kodo strani z obrazcem, lahko opazite, da Nette obvezne elemente vstavlja v elemente s CSS razredom
required
. Poskusite dodati v predlogo naslednji slogovni list in oznaka „Ime“ bo rdeča. Elegantno tako
uporabnikom označimo obvezne elemente:
<style>
.required label { color: maroon }
</style>
Druga validacijska pravila dodamo z metodo addRule()
. Prvi parameter je pravilo, drugi je spet besedilo
sporočila o napaki in lahko še sledi argument validacijskega pravila. Kaj se s tem misli?
Obrazec razširimo z novim neobveznim poljem „starost“, ki mora biti celo število (addInteger()
) in poleg
tega v dovoljenem obsegu ($form::Range
). In tukaj prav izkoristimo tretji parameter metode addRule()
,
s katerim validatorju predamo zahtevani obseg kot par [od, do]
:
$form->addInteger('age', 'Starost:')
->addRule($form::Range, 'Starost mora biti od 18 do 120', [18, 120]);
Če uporabnik polja ne izpolni, se validacijska pravila ne bodo preverjala, saj je element neobvezen.
Tukaj nastane prostor za drobno preoblikovanje (refactoring). V sporočilu o napaki in v tretjem parametru so števila
navedena podvojeno, kar ni idealno. Če bi ustvarjali večjezične obrazce in bi bilo sporočilo, ki vsebuje
števila, prevedeno v več jezikov, bi se otežila morebitna sprememba vrednosti. Iz tega razloga je mogoče uporabiti nadomestne
znake %d
in Nette vrednosti dopolni:
->addRule($form::Range, 'Starost mora biti od %d do %d let', [18, 120]);
Vrnimo se k elementu password
, ki ga prav tako naredimo obveznega in še preverimo minimalno dolžino gesla
($form::MinLength
), spet z uporabo nadomestnega znaka:
$form->addPassword('password', 'Geslo:')
->setRequired('Izberite si geslo')
->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8);
Dodamo v obrazec še polje passwordVerify
, kjer uporabnik vnese geslo še enkrat, za preverjanje. S pomočjo
validacijskih pravil preverimo, ali sta obe gesli enaki ($form::Equal
). In kot parameter damo sklic na prvo geslo
s pomočjo oglatih oklepajev:
$form->addPassword('passwordVerify', 'Geslo za preverjanje:')
->setRequired('Prosimo, vnesite geslo še enkrat za preverjanje')
->addRule($form::Equal, 'Gesli se ne ujemata', $form['password'])
->setOmitted();
S pomočjo setOmitted()
smo označili element, katerega vrednost nas pravzaprav ne zanima in ki obstaja le zaradi
validacije. Vrednost se ne preda v $data
.
S tem imamo končan popolnoma delujoč obrazec z validacijo v PHP in JavaScriptu. Validacijske sposobnosti Nette so veliko širše, dajo se ustvarjati pogoji, puščati glede na njih prikazovati in skrivati dele strani itd. Vse se boste naučili v poglavju o validaciji obrazcev.
Privzete vrednosti
Elementom obrazca običajno nastavljamo privzete vrednosti:
$form->addEmail('email', 'E-pošta')
->setDefaultValue($lastUsedEmail);
Pogosto je koristno nastaviti privzete vrednosti vsem elementom hkrati. Na primer, ko obrazec služi za urejanje zapisov. Preberemo zapis iz podatkovne baze in nastavimo privzete vrednosti:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Kličite setDefaults()
šele po definiciji elementov.
Izrisovanje obrazca
Standardno se obrazec izriše kot tabela. Posamezni elementi izpolnjujejo osnovno pravilo dostopnosti – vse oznake so
zapisane kot <label>
in povezane z ustreznim elementom obrazca. Ob kliku na oznako se kazalec samodejno pojavi
v polju obrazca.
Vsakemu elementu lahko nastavljamo poljubne HTML atribute. Na primer dodati placeholder:
$form->addInteger('age', 'Starost:')
->setHtmlAttribute('placeholder', 'Prosimo, izpolnite starost');
Načinov, kako izrisati obrazec, je res veliko, zato je temu namenjeno samostojno poglavje o izrisovanju.
Mapiranje na razrede
Vrnimo se k metodi formSucceeded()
, ki v drugem parametru $data
prejme poslane podatke kot objekt
ArrayHash
. Ker gre za generični razred, nekaj kot stdClass
, nam bo pri delu z njim manjkalo določeno
udobje, kot je na primer predlaganje lastnosti v urejevalnikih ali statična analiza kode. To bi lahko rešili tako, da bi za
vsak obrazec imeli konkreten razred, katerega lastnosti predstavljajo posamezne elemente. Npr.:
class RegistrationFormData
{
public string $name;
public ?int $age;
public string $password;
}
Alternativno lahko uporabite konstruktor:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Lastnosti podatkovnega razreda so lahko tudi enumi in pride do njihovega samodejnega mapiranja.
Kako povedati Nette, naj nam podatke vrača kot objekte tega razreda? Lažje, kot si mislite. Zadostuje le navesti razred kot
tip parametra $data
v obdelovalni metodi:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name je instanca RegistrationFormData
$name = $data->name;
// ...
}
Kot tip lahko navedete tudi array
in potem podatke preda kot polje.
Podobno lahko uporabljate tudi funkcijo getValues()
, ki ji ime razreda ali objekt za hidracijo predamo kot
parameter:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Če obrazci tvorijo večnivojsko strukturo, sestavljeno iz vsebnikov, ustvarite za vsakega samostojen razred:
$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;
}
Mapiranje nato iz tipa lastnosti $person
prepozna, da mora vsebnik mapirati na razred PersonFormData
.
Če bi lastnost vsebovala polje vsebnikov, navedite tip array
in razred za mapiranje predajte neposredno
vsebniku:
$person->setMappedType(PersonFormData::class);
Načrt podatkovnega razreda obrazca si lahko pustite generirati s pomočjo metode
Nette\Forms\Blueprint::dataClass($form)
, ki ga izpiše na stran brskalnika. Kodo nato zadostuje s klikom označiti
in kopirati v projekt.
Več gumbov
Če ima obrazec več kot en gumb, moramo praviloma razlikovati, kateri od njih je bil pritisnjen. Lahko si za vsak gumb
ustvarimo lastno obdelovalno funkcijo. Nastavimo jo kot handler za dogodek onClick
:
$form->addSubmit('save', 'Shrani')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Izbriši')
->onClick[] = [$this, 'deleteButtonPressed'];
Ti handlerji se kličejo le v primeru veljavno izpolnjenega obrazca, enako kot v primeru dogodka onSuccess
.
Razlika je v tem, da se kot prvi parameter namesto obrazca lahko preda gumb za oddajo, odvisno od tipa, ki ga navedete:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Ko se obrazec odda z gumbom Enter, se šteje, kot da bi bil oddan s prvim gumbom.
Dogodek onAnchor
Ko v tovarni metodi (kot je npr. createComponentRegistrationForm
) sestavljamo obrazec, ta še ne ve, ali je bil
poslan, niti s kakšnimi podatki. So pa primeri, ko poslane vrednosti potrebujemo poznati, na primer se glede na njih odvija
nadaljnja podoba obrazca, ali jih potrebujemo za odvisna izbirna polja itd.
Del kode, ki sestavlja obrazec, lahko zato pustite poklicati šele v trenutku, ko je t.i. zasidran, torej je že povezan
s presenterjem in pozna svoje poslane podatke. Takšno kodo predamo v polje $onAnchor
:
$country = $form->addSelect('country', 'Država:', $this->model->getCountries());
$city = $form->addSelect('city', 'Mesto:');
$form->onAnchor[] = function () use ($country, $city) {
// ta funkcija se pokliče šele, ko bo obrazec vedel, ali je bil poslan in s kakšnimi podatki
// lahko torej uporabljamo metodo getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Zaščita pred ranljivostmi
Nette Framework daje velik poudarek varnosti in zato skrbno pazi na dobro zavarovanje obrazcev. To počne popolnoma transparentno in ne zahteva ročnega nastavljanja ničesar.
Poleg tega, da obrazce zaščiti pred napadom Cross Site Scripting (XSS) in Cross-Site Request Forgery (CSRF), opravlja veliko drobnih zavarovanj, na katere vam ni več treba misliti.
Tako na primer iz vnosov odfiltrira vse kontrolne znake in preveri veljavnost UTF-8 kodiranja, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in seznamih izbirnih gumbov preverja, da so bile izbrane postavke dejansko iz ponujenih in da ni prišlo do ponarejanja. Že smo omenili, da pri enovrstičnih besedilnih vnosih odstranjuje znake konca vrstic, ki bi jih tja lahko poslal napadalec. Pri večvrstičnih vnosih pa normalizira znake za konce vrstic. In tako naprej.
Nette za vas rešuje varnostna tveganja, za katera veliko programerjev niti ne ve, da obstajajo.
Omenjeni CSRF napad temelji na tem, da napadalec žrtev zvabi na stran, ki neopazno v brskalniku žrtve izvede zahtevo na strežnik, na katerem je žrtev prijavljena, in strežnik domneva, da je zahtevo izvedla žrtev po svoji volji. Zato Nette preprečuje pošiljanje POST obrazca iz druge domene. Če iz kakršnega koli razloga želite zaščito izklopiti in dovoliti pošiljanje obrazca iz druge domene, uporabite:
$form->allowCrossOrigin(); // POZOR! Izklopi zaščito!
Ta zaščita uporablja SameSite piškotek, poimenovan _nss
. Zaščita s pomočjo SameSite piškotka morda ni 100%
zanesljiva, zato je priporočljivo vklopiti še zaščito s pomočjo žetona (token):
$form->addProtection();
Priporočamo, da tako zaščitite obrazce v administrativnem delu spletnega mesta, ki spreminjajo občutljive podatke
v aplikaciji. Ogrodje se proti napadu CSRF brani z generiranjem in preverjanjem avtorizacijskega žetona, ki se shranjuje
v sejo. Zato je treba pred prikazom obrazca imeti odprto sejo. V administrativnem delu spletnega mesta je običajno seja že
zagnana zaradi prijave uporabnika. Sicer sejo zaženite z metodo Nette\Http\Session::start()
.
Enak obrazec v več presenterjih
Če potrebujete en obrazec uporabiti v več presenterjih, priporočamo, da si zanj ustvarite tovarno, ki si jo nato predajte
v presenter. Primerna lokacija za tak razred je npr. imenik app/Forms
.
Tovarniški razred lahko izgleda na primer takole:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Ime:');
$form->addSubmit('send', 'Prijavi se');
return $form;
}
}
Razred prosimo za izdelavo obrazca v tovarni metodi za komponente v presenterju:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// lahko obrazec spremenimo, tukaj na primer spreminjamo napis na gumbu
$form['send']->setCaption('Nadaljuj');
$form->onSuccess[] = [$this, 'signInFormSuceeded']; // in dodamo handler
return $form;
}
Handler za obdelavo obrazca je lahko tudi že dodan iz tovarne:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Ime:');
$form->addSubmit('send', 'Prijavi se');
$form->onSuccess[] = function (Form $form, $data): void {
// tukaj izvedemo obdelavo obrazca
};
return $form;
}
}
Tako, za nami je hiter uvod v obrazce v Nette. Poskusite še pogledati v imenik examples v distribuciji, kjer najdete dodatno inspiracijo.