Obrazci v programu Presenters
Nette Forms bistveno olajša ustvarjanje in obdelavo spletnih obrazcev. V tem poglavju se boste naučili, kako uporabljati obrazce v predstavitvah.
Če jih želite uporabljati povsem samostojno brez preostalega ogrodja, je na voljo vodnik za samostojne obrazce.
Prvi obrazec
Poskusili bomo napisati preprost obrazec za registracijo. Njegova koda bo videti takole:
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'];
v brskalniku pa mora biti rezultat videti takole:
Obrazec v predstavitvi je objekt razreda Nette\Application\UI\Form
, njegov predhodnik
Nette\Forms\Form
pa je namenjen samostojni uporabi. Dodali smo mu polja ime, geslo in gumb za pošiljanje. Nazadnje
je v vrstici s $form->onSuccess
zapisano, da je treba po oddaji in uspešni potrditvi poklicati metodo
$this->formSucceeded()
.
Z vidika predstavnika je obrazec skupna komponenta. Zato ga obravnavamo kot komponento in ga vključimo v predstavitveni program z uporabo tovarniške metode. To bo videti 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', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// tukaj bomo obdelali podatke, ki jih je poslal obrazec
// $data->name vsebuje ime
// $data->password vsebuje geslo
$this->flashMessage('You have successfully signed up.');
$this->redirect('Home:');
}
}
Prikaz v predlogi pa se izvede z uporabo oznake {control}
:
<h1>Registration</h1>
{control registrationForm}
In to je vse :-) Imamo funkcionalen in popolnoma zaščiten obrazec.
Zdaj verjetno razmišljate, da je bilo to prehitro, in se sprašujete, kako je mogoče, da se kliče metoda
formSucceeded()
in kakšne parametre dobi. Seveda imate prav, to si zasluži razlago.
Nette se je domislil kul mehanizma, ki mu pravimo hollywoodski slog. Namesto da bi nenehno spraševali, ali se je nekaj zgodilo (“je bil obrazec oddan?”, “je bil veljavno oddan?” ali “ni bil ponarejen?”), ogrodju rečete “ko je obrazec veljavno izpolnjen, pokliči to metodo” in mu prepustite nadaljnje delo. Če programirate v javascriptu, ste seznanjeni s tem načinom programiranja. Napišete funkcije, ki se pokličejo, ko se zgodi določen dogodek. Jezik pa jim posreduje ustrezne argumente.
Na ta način je zgrajena zgornja koda predstavnika. Polje $form->onSuccess
predstavlja seznam povratnih klicev
PHP, ki jih bo Nette poklical, ko bo obrazec oddan in pravilno izpolnjen. V življenjskem ciklu predstavnika gre za
tako imenovani signal, zato se kličejo po metodi action*
in pred metodo render*
. Vsakemu povratnemu
klicu pa posreduje sam obrazec v prvem parametru in poslane podatke kot objekt ArrayHash v drugem. Prvi parameter lahko izpustite, če objekta
obrazca ne potrebujete. Drugi parameter je lahko še bolj priročen, a o tem kasneje.
Objekt $data
vsebuje lastnosti name
in password
s podatki, ki jih je vnesel uporabnik.
Običajno podatke pošljemo neposredno v nadaljnjo obdelavo, ki je lahko na primer vstavljanje v zbirko podatkov. Vendar pa
lahko med obdelavo pride do napake, na primer uporabniško ime je že zasedeno. V tem primeru posredujemo napako nazaj obrazcu
s pomočjo addError()
in ga pustimo ponovno narisati, s sporočilom o napaki:
$form->addError('Sorry, username is already in use.');
Poleg onSuccess
obstaja tudi onSubmit
: povratni klici se vedno pokličejo po oddaji obrazca, tudi če
ta ni pravilno izpolnjen. In končno onError
: klicne vrstice se pokličejo samo, če oddaja ni pravilna. Pokličejo
se celo, če z uporabo addError()
razveljavimo obrazec v onSuccess
ali onSubmit
.
Po obdelavi obrazca bomo preusmerili na naslednjo stran. S tem preprečimo, da bi obrazec nenamerno ponovno oddali s klikom na gumb osvežitev, povratek ali s premikanjem zgodovine brskalnika.
Poskusite dodati več kontrolnih elementov obrazca.
Dostop do kontrolnih elementov
Obrazec je sestavni del predstavnika, v našem primeru poimenovanega registrationForm
(po imenu tovarniške
metode createComponentRegistrationForm
), zato lahko kjer koli v predstavniku pridete do obrazca z uporabo:
$form = $this->getComponent('registrationForm');
// alternativna sintaksa: $form = $this['registrationForm'];
Tudi posamezne kontrole obrazca so komponente, zato lahko do njih dostopate na enak način:
$input = $form->getComponent('name'); // ali $input = $form['name'];
$button = $form->getComponent('send'); // ali $button = $form['send'];
Kontrolne elemente odstranite z uporabo funkcije unset:
unset($form['name']);
Pravila potrjevanja
Večkrat je bila uporabljena beseda valid, vendar obrazec še nima pravil potrjevanja. To popravimo.
Ime bo obvezno, zato ga bomo označili z metodo setRequired()
, katere argument je besedilo sporočila o napaki,
ki se prikaže, če ga uporabnik ne izpolni. Če argument ni naveden, se uporabi privzeto sporočilo o napaki.
$form->addText('name', 'Name:')
->setRequired('Please fill your name.');
Poskusite oddati obrazec brez izpolnjenega imena in videli boste, da se bo prikazalo sporočilo o napaki, brskalnik ali strežnik pa ga bo zavrnil, dokler ga ne izpolnite.
Hkrati sistema ne boste mogli prevarati tako, da boste v vnos vnesli samo presledke, na primer. Na noben način. Nette samodejno obreže leve in desne bele prostore. Preizkusite. To je nekaj, kar bi morali vedno narediti pri vsakem enovrstičnem vnosu, vendar se na to pogosto pozablja. Nette to stori samodejno. (Lahko poskusite prevarati obrazce in kot ime pošljete večvrstični niz. Tudi v tem primeru se Nette ne bo pustil preslepiti in prelomi vrstic se bodo spremenili v presledke.)
Obrazec se vedno potrdi na strani strežnika, vendar se ustvari tudi potrditev JavaScript, ki je hitra in uporabnik takoj izve
za napako, ne da bi mu bilo treba obrazec poslati v strežnik. Za to poskrbi skripta netteForms.js
. Vstavite jo
v predlogo postavitve:
<script src="https://unpkg.com/nette-forms@3"></script>
Če pogledate izvorno kodo strani z obrazcem, lahko opazite, da Nette vstavi zahtevana polja v elemente z razredom CSS
required
. Poskusite v predlogo dodati naslednji slog in oznaka “Ime” bo rdeča. Na eleganten način označimo
zahtevana polja za uporabnike:
<style>
.required label { color: maroon }
</style>
Dodatna pravila potrjevanja bomo dodali z metodo addRule()
. Prvi parameter je pravilo, drugi je spet besedilo
sporočila o napaki, sledi pa lahko neobvezni argument pravila potrjevanja. Kaj to pomeni?
Obrazec bo dobil še en izbirni vnos starost s pogojem, da mora biti številka (addInteger()
) in
v določenih mejah ($form::Range
). In tu bomo uporabili tretji argument addRule()
, tj. samo
območje:
$form->addInteger('age', 'Age:')
->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);
Če uporabnik polja ne izpolni, se pravila potrjevanja ne bodo preverila, saj je polje neobvezno.
Očitno je na voljo prostor za majhno preoblikovanje. V sporočilu o napaki in v tretjem parametru so številke navedene
podvojeno, kar ni idealno. Če bi ustvarjali večjezični
obrazec in bi bilo treba sporočilo s številkami prevesti v več jezikov, bi bilo spreminjanje vrednosti težje. Zato lahko
uporabimo nadomestne znake %d
:
->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);
Vrnimo se k polju geslo, naredimo ga zahtevno in preverimo najmanjšo dolžino gesla
($form::MinLength
), pri čemer ponovno uporabimo nadomestne znake v sporočilu:
$form->addPassword('password', 'Password:')
->setRequired('Pick a password')
->addRule($form::MinLength, 'Your password has to be at least %d long', 8);
Za preverjanje bomo obrazcu dodali polje passwordVerify
, kamor uporabnik ponovno vnese geslo. Z uporabo pravil
potrjevanja preverimo, ali sta obe gesli enaki ($form::Equal
). Kot argument pa podamo sklic na prvo geslo z uporabo
oglatih oklepajev:
$form->addPassword('passwordVerify', 'Password again:')
->setRequired('Fill your password again to check for typo')
->addRule($form::Equal, 'Password mismatch', $form['password'])
->setOmitted();
Z uporabo setOmitted()
smo označili element, katerega vrednost nas v resnici ne zanima in ki obstaja le za
potrjevanje. Njegove vrednosti ne posredujemo v $data
.
Imamo popolnoma funkcionalen obrazec s preverjanjem v PHP in JavaScript. Nettejeve možnosti potrjevanja so veliko širše, saj lahko ustvarite pogoje, v skladu z njimi prikažete in skrijete dele strani itd. Vse to si lahko preberete v poglavju o potrjevanju obrazcev.
Privzete vrednosti
Pogosto nastavljamo privzete vrednosti za kontrolne elemente obrazca:
$form->addEmail('email', 'Email')
->setDefaultValue($lastUsedEmail);
Pogosto je koristno nastaviti privzete vrednosti za vse kontrole naenkrat. Na primer, ko se obrazec uporablja za urejanje zapisov. Zapise preberemo iz podatkovne zbirke in jih nastavimo kot privzete vrednosti:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Po definiranju kontrolnih elementov pokličemo setDefaults()
.
Prikazovanje obrazca
Obrazec je privzeto prikazan kot tabela. Posamezni kontrolni elementi upoštevajo osnovne smernice spletne dostopnosti. Vse
oznake so ustvarjene kot <label>
elementi in so povezani z njihovimi vhodi, klik na etiketo pa premakne
kazalec na vhod.
Za vsak element lahko nastavimo poljubne atribute HTML. Dodajte na primer nadomestno mesto:
$form->addInteger('age', 'Age:')
->setHtmlAttribute('placeholder', 'Please fill in the age');
V tem poglavju je veliko načinov upodabljanja obrazca, zato je namenjeno prav upodabljanju.
Prikazovanje v razrede
Vrnimo se k metodi formSucceeded()
, ki v drugem parametru $data
dobi poslane podatke kot objekt
ArrayHash
. Ker gre za splošen razred, nekaj podobnega kot stdClass
, bomo pri delu z njim prikrajšani
za nekaj udobja, na primer za dopolnjevanje kode za lastnosti v urejevalnikih ali statično analizo kode. To bi lahko rešili
tako, da bi za vsak obrazec imeli poseben razred, katerega lastnosti bi predstavljale posamezne kontrole. Npr:
class RegistrationFormData
{
public string $name;
public int $age;
public string $password;
}
Uporabite lahko tudi konstruktor:
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
Lastnosti podatkovnega razreda so lahko tudi enumi in bodo samodejno preslikani.
Kako reči Nette, naj vrne podatke kot predmete tega razreda? Lažje, kot si mislite. Vse, kar morate storiti, je določiti
razred kot tip parametra $data
v izvajalcu:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name je primerek RegistrationFormData
$name = $data->name;
// ...
}
Kot tip lahko določite tudi array
in potem bo posredoval podatke kot polje.
Na podoben način lahko uporabite metodo getValues()
, ki ji kot parameter posredujemo ime razreda ali predmeta
hidrata:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Če so obrazci sestavljeni iz večnivojske strukture, sestavljene iz vsebnikov, ustvarite ločen razred za vsakega od njih:
$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;
}
Prikazovanje nato na podlagi vrste lastnosti $person
ugotovi, da mora vsebnik prikazati v razred
PersonFormData
. Če bi lastnost vsebovala polje vsebnikov, navedite tip array
in razred, ki ga je treba
preslikati neposredno na vsebnik:
$person->setMappedType(PersonFormData::class);
Predlog podatkovnega razreda obrazca lahko ustvarite z metodo
Nette\Forms\Blueprint::dataClass($form)
, ki ga izpiše na strani brskalnika. Nato lahko preprosto kliknete, da
izberete in kopirate kodo v svoj projekt.
Več gumbov za pošiljanje
Če ima obrazec več kot en gumb, moramo običajno razlikovati, kateri je bil pritisnjen. Za vsak gumb lahko ustvarimo svojo
funkcijo. Nastavite jo kot izvajalca za dogodek
onClick
:
$form->addSubmit('save', 'Save')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Delete')
->onClick[] = [$this, 'deleteButtonPressed'];
Tudi ti obdelavci se kličejo samo v primeru, da je obrazec veljaven, kot v primeru dogodka onSuccess
. Razlika
je v tem, da je lahko prvi parameter objekt gumba za pošiljanje namesto obrazca, odvisno od vrste, ki ste jo določili:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Ko je obrazec oddan s tipko Enter, se obravnava, kot da bi bil oddan s prvim gumbom.
Dogodek onAnchor
Ko obrazec sestavite s tovarniško metodo (kot je createComponentRegistrationForm
), ta še ne ve, ali je bil
oddan ali s kakšnimi podatki je bil oddan. Obstajajo pa primeri, ko moramo poznati oddane vrednosti, morda je od njih odvisno,
kako bo obrazec videti, ali pa se uporabljajo za odvisna izbirna polja itd.
Zato lahko kodo, ki zgradi obrazec, pokličete, ko je obrazec zasidran, tj. je že povezan s predstavnikom in pozna njegove
oddane podatke. Takšno kodo bomo postavili v polje $onAnchor
:
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');
$form->onAnchor[] = function () use ($country, $city) {
// ta funkcija se pokliče, ko obrazec pozna podatke, s katerimi je bil poslan.
// zato lahko uporabite metodo getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val): []);
};
Zaščita pred ranljivostmi
Okvir Nette si zelo prizadeva biti varen, in ker so obrazci najpogostejši uporabniški vnos, so obrazci Nette tako dobro kot neprepustni. Vse se vzdržuje dinamično in pregledno, ničesar ni treba nastavljati ročno.
Poleg zaščite obrazcev pred napadi, usmerjenimi v dobro znane ranljivosti, kot sta Cross-Site Scripting (XSS ) in Cross-Site Request Forgery (CSRF), opravi tudi veliko manjših varnostnih opravil, o katerih vam ni treba več razmišljati.
Na primer, iz vnosov filtrira vse kontrolne znake in preverja veljavnost kodiranja UTF-8, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in radijskih seznamih preveri, ali so bili izbrani elementi dejansko izmed ponujenih in ni prišlo do ponarejanja. Omenili smo že, da pri enovrstičnem vnosu besedila odstrani znake s konca vrstice, ki bi jih lahko tja poslal napadalec. Pri večvrstičnih vnosih normalizira znake na koncu vrstice. In tako naprej.
Nette za vas odpravi varnostne ranljivosti, za katere večina programerjev nima pojma, da obstajajo.
Pri omenjenem napadu CSRF gre za to, da napadalec žrtev zvabi k obisku strani, ki v brskalniku žrtve tiho izvrši zahtevo strežniku, v katerega je žrtev trenutno prijavljena, strežnik pa verjame, da je zahtevo po svoji volji izvršila žrtev. Zato Nette prepreči, da bi bil obrazec oddan prek protokola POST iz druge domene. Če iz nekega razloga želite izklopiti zaščito in dovoliti oddajo obrazca iz druge domene, uporabite:
$form->allowCrossOrigin(); // POZOR! Izklopi zaščito!
Ta zaščita uporablja piškotek SameSite z imenom _nss
. Zaščita s piškotki SameSite morda ni 100-odstotno
zanesljiva, zato je dobro vklopiti zaščito s simboli:
$form->addProtection();
To zaščito je zelo priporočljivo uporabiti za obrazce v upravnem delu aplikacije, ki spreminjajo občutljive podatke. Okvir
ščiti pred napadom CSRF z generiranjem in potrjevanjem avtentikacijskega žetona, ki je shranjen v seji (argument je
sporočilo o napaki, ki se prikaže, če je žeton potekel). Zato je treba pred prikazom obrazca zagnati sejo. V upravljalnem
delu spletnega mesta je seja običajno že začeta zaradi prijave uporabnika. V nasprotnem primeru sejo zaženite z metodo
Nette\Http\Session::start()
.
Uporaba enega obrazca v več prikazovalnikih
Če morate en obrazec uporabiti v več kot enem predstavitvenem programu, priporočamo, da zanj ustvarite tovarno, ki jo nato
posredujete predstavitvenemu programu. Primerno mesto za tak razred je na primer imenik app/Forms
.
Tovarniški razred je lahko videti takole:
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;
}
}
Razred prosimo, da v tovarniški metodi izdela obrazec za komponente v predstavitvenem programu:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// lahko spremenimo obrazec, tukaj na primer spremenimo etiketo na gumbu
$form['login']->setCaption('Continue');
$form->onSuccess[] = [$this, 'signInFormSubmitted']; // in dodamo izvajalca
return $form;
}
Obdelovalec za obdelavo obrazca se lahko prav tako dostavi iz tovarne:
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 {
// tukaj obdelamo oddani obrazec
};
return $form;
}
}
Tako smo na hitro spoznali obrazce v Nette. Za več navdiha si oglejte imenik s primeri v distribuciji.