Nette Documentation Preview

syntax
Obrazci v programu Presenters
*****************************

.[perex]
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 |standalone].


Prvi obrazec .[#toc-first-form]
===============================

Poskusili bomo napisati preprost obrazec za registracijo. Njegova koda bo videti takole:

```php
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:

[* form-en.webp *]

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 |application:components#Factory Methods]. To bo videti takole:

```php .{file:app/UI/Home/HomePresenter.php}
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}`:

```latte .{file:app/UI/Home/default.latte}
<h1>Registration</h1>

{control registrationForm}
```

In to je vse :-) Imamo funkcionalen in popolnoma [zaščiten |#Vulnerability Protection] 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 |application:components#Hollywood style]. 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 |nette:glossary#Events]. 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 |application:presenters#Life Cycle of Presenter] 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 |utils:arrays#ArrayHash] v drugem. Prvi parameter lahko izpustite, če objekta obrazca ne potrebujete. Drugi parameter je lahko še bolj priročen, a o tem [kasneje |#Mapping to Classes].

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:

```php
$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 |controls].


Dostop do kontrolnih elementov .[#toc-access-to-controls]
=========================================================

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:

```php
$form = $this->getComponent('registrationForm');
// alternativna sintaksa: $form = $this['registrationForm'];
```

Tudi posamezne kontrole obrazca so komponente, zato lahko do njih dostopate na enak način:

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

Kontrolne elemente odstranite z uporabo funkcije unset:

```php
unset($form['name']);
```


Pravila potrjevanja .[#toc-validation-rules]
============================================

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.

```php
$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:

```latte
<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></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:

```latte
<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:

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

.[tip]
Č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 |rendering#translating] in bi bilo treba sporočilo s številkami prevesti v več jezikov, bi bilo spreminjanje vrednosti težje. Zato lahko uporabimo nadomestne znake `%d`:

```php
	->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:

```php
$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 |#Access to Controls]:

```php
$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 |validation].


Privzete vrednosti .[#toc-default-values]
=========================================

Pogosto nastavljamo privzete vrednosti za kontrolne elemente obrazca:

```php
$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:

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

Po definiranju kontrolnih elementov pokličemo `setDefaults()`.


Prikazovanje obrazca .[#toc-rendering-the-form]
===============================================

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:

```php
$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 |rendering].


Prikazovanje v razrede .[#toc-mapping-to-classes]
=================================================

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:

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

Od različice PHP 8.0 lahko uporabite eleganten zapis, ki uporablja konstruktor:

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

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:

```php
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:

```php
$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:

```php
$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:

```php
$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. .{data-version:3.1.15}


Več gumbov za pošiljanje .[#toc-multiple-submit-buttons]
========================================================

Č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 |nette:glossary#Events] `onClick`:

```php
$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:

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

Ko je obrazec oddan s tipko <kbd>Enter</kbd>, se obravnava, kot da bi bil oddan s prvim gumbom.


Dogodek onAnchor .[#toc-event-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`:

```php
$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 .[#toc-vulnerability-protection]
==========================================================

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 |nette:glossary#cross-site-scripting-xss] ) in [Cross-Site Request Forgery (CSRF) |nette:glossary#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:

```php
$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:

```php
$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 .[#toc-using-one-form-in-multiple-presenters]
=========================================================================================

Č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:

```php
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:

```php
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:

```php
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 |https://github.com/nette/forms/tree/master/examples] v distribuciji.

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/src/assets/netteForms.js"></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;
}

Od različice PHP 8.0 lahko uporabite eleganten zapis, ki uporablja konstruktor:

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

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.