Nette Documentation Preview

syntax
Formularze używane samodzielnie
*******************************

.[perex]
Nette Forms dramatycznie upraszczają tworzenie i przetwarzanie formularzy webowych. Możesz ich używać w swoich aplikacjach całkowicie samodzielnie, bez reszty frameworku, co pokazujemy w tym rozdziale.

Jeśli jednak używasz Nette Application i presenterów, jest dla Ciebie osobny przewodnik: [formularze w presenterach |in-presenter].


Pierwszy formularz
==================

Zanim zaczniesz, zainstaluj pakiet za pomocą [Composera |best-practices:composer]:

```shell
composer require nette/forms
```

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak ("pełny kod":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851):

```php
use Nette\Forms\Form;

$form = new Form;
$form->addText('name', 'Imię:');
$form->addPassword('password', 'Hasło:');
$form->addSubmit('send', 'Zarejestruj się');
```

I wyrenderujmy go bardzo prosto:

```php
$form->render();
```

Wynik w przeglądarce powinien wyglądać tak:

[* form-en.webp *]

Formularz jest obiektem klasy `Nette\Forms\Form` (klasa `Nette\Application\UI\Form` używana jest w presenterach). Dodaliśmy do niego elementy o nazwach "name", "password" oraz przycisk wysyłający.

Teraz ożywmy formularz. Zapytaniem `$form->isSuccess()` sprawdzimy, czy formularz został wysłany i czy był poprawnie wypełniony. Jeśli tak, wypiszemy dane. Za definicją formularza dopiszemy:

```php
if ($form->isSuccess()) {
	echo 'Formularz został poprawnie wypełniony i wysłany';
	$data = $form->getValues();
	// $data->name zawiera imię
	// $data->password zawiera hasło
	var_dump($data);
}
```

Metoda `getValues()` zwraca wysłane dane jako obiekt [ArrayHash |utils:arrays#ArrayHash]. Jak to zmienić, pokażemy [później |#Mapowanie na klasy]. Obiekt `$data` zawiera klucze `name` i `password` z danymi wpisanymi przez użytkownika.

Zwykle wysyłamy dane bezpośrednio do dalszego przetwarzania, którym może być na przykład zapis do bazy danych. Podczas przetwarzania może jednak dojść do błędu, na przykład nazwa użytkownika jest już zajęta. W takim przypadku przekazujemy błąd z powrotem do formularza za pomocą `addError()` i pozwalamy go wyrenderować ponownie, wraz z komunikatem o błędzie.

```php
$form->addError('Przepraszamy, ta nazwa użytkownika jest już zajęta.');
```

Po przetworzeniu formularza przekierowujemy na kolejną stronę. Zapobiega to niezamierzonemu ponownemu wysłaniu formularza przyciskiem *odśwież* albo *wstecz* czy przez przemieszczanie się po historii przeglądarki.

Domyślnie formularz wysyłany jest metodą POST na tę samą stronę. Jedno i drugie da się zmienić:

```php
$form->setAction('/submit.php');
$form->setMethod('GET');
```

I to w zasadzie wszystko :-) Mamy działający i doskonale [zabezpieczony |#Ochrona przed podatnościami] formularz.

Spróbuj dodać też inne [elementy formularza |controls].


Dostęp do elementów
===================

Formularz i jego poszczególne elementy nazywamy komponentami. Tworzą one drzewo komponentów, którego korzeniem jest formularz. Do poszczególnych elementów formularza dostaniesz się tak:

```php
$input = $form->getComponent('name');
// alternatywna składnia: $input = $form['name'];

$button = $form->getComponent('send');
// alternatywna składnia: $button = $form['send'];
```

Elementy usuwa się za pomocą `unset`:

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


Reguły walidacyjne
==================

Padło słowo *valid*, ale formularz nie ma jeszcze żadnych reguł walidacyjnych. Naprawmy to.

Imię będzie obowiązkowe, więc oznaczymy je metodą `setRequired()`. Jej argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik imienia nie wypełni. Jeśli argumentu nie podamy, użyty zostanie domyślny komunikat o błędzie.

```php
$form->addText('name', 'Imię:')
	->setRequired('Podaj imię.');
```

Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że pojawi się komunikat o błędzie. Przeglądarka albo serwer odrzuci go, dopóki pola nie wypełnisz.

Jednocześnie systemu nie oszukasz, wpisując do inputu same spacje. Nie ma szans. Nette automatycznie przycina białe znaki z lewej i prawej strony. Wypróbuj to. To coś, co powinieneś zawsze robić z każdym jednoliniowym inputem, ale o czym często się zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i wysłać jako imię ciąg wieloliniowy. Nawet tutaj Nette nie da się nabrać, a złamania linii zostaną zamienione na spacje.)

Formularz jest zawsze walidowany po stronie serwera, ale generowana jest też walidacja w JavaScripcie. Ta działa błyskawicznie i użytkownik dowiaduje się o błędzie natychmiast, bez potrzeby wysyłania formularza na serwer. Zajmuje się tym skrypt `netteForms.js`. Wstaw go na stronę:

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

Jeśli zajrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette wstawia obowiązkowe elementy do elementów z klasą CSS `required`. Spróbuj dodać do szablonu poniższy arkusz stylów, a etykieta "Imię" stanie się czerwona. Elegancko oznaczysz w ten sposób użytkownikom elementy obowiązkowe:

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

Kolejne reguły walidacyjne dodajemy metodą `addRule()`. Pierwszym parametrem jest reguła, drugim znów tekst komunikatu o błędzie, a dalej może następować opcjonalny argument reguły walidacyjnej. Co to znaczy?

Rozszerzmy formularz o nowe, opcjonalne pole "wiek", które musi być liczbą całkowitą (`addInteger()`) i mieścić się w dozwolonym przedziale (`$form::Range`). I tutaj wykorzystamy trzeci parametr metody `addRule()`, którym przekażemy walidatorowi wymagany przedział jako parę `[min, max]`:

```php
$form->addInteger('age', 'Wiek:')
	->addRule($form::Range, 'Wiek musi mieścić się między 18 a 120.', [18, 120]);
```

.[tip]
Jeśli użytkownik pola nie wypełni, reguły walidacyjne nie będą sprawdzane, bo element jest opcjonalny.

Powstaje tu miejsce na drobny refaktoring. W komunikacie o błędzie i w trzecim parametrze liczby są zduplikowane, co nie jest idealne. Gdybyśmy tworzyli [formularze wielojęzyczne |rendering#Tłumaczenie] i komunikat zawierający liczby byłby przetłumaczony na kilka języków, zmiana wartości stałaby się trudna. Z tego powodu można użyć zastępników `%d`, a Nette wartości uzupełni:

```php
	->addRule($form::Range, 'Wiek musi mieścić się między %d a %d lat.', [18, 120]);
```

Wróćmy do elementu `password`, uczyńmy go również obowiązkowym i sprawdźmy jeszcze minimalną długość hasła (`$form::MinLength`), znów z użyciem zastępnika w komunikacie:

```php
$form->addPassword('password', 'Hasło:')
	->setRequired('Wybierz hasło')
	->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8);
```

Dodajmy do formularza jeszcze pole `passwordVerify`, w którym użytkownik wpisze hasło ponownie dla kontroli. Za pomocą reguł walidacyjnych sprawdzimy, czy oba hasła są takie same (`$form::Equal`). Jako parametr podamy odwołanie do pierwszego hasła za pomocą [nawiasów kwadratowych |#Dostęp do elementów]:

```php
$form->addPassword('passwordVerify', 'Hasło ponownie:')
	->setRequired('Wpisz hasło jeszcze raz dla kontroli')
	->addRule($form::Equal, 'Hasła nie są zgodne', $form['password'])
	->setOmitted();
```

Za pomocą `setOmitted()` oznaczyliśmy element, którego wartość właściwie nas nie interesuje i który istnieje tylko na potrzeby walidacji. Jego wartość nie jest przekazywana do `$data`.

Tym samym mamy w pełni działający formularz z walidacją w PHP i JavaScripcie. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, na ich podstawie pokazywać i ukrywać części strony itd. Wszystkiego dowiesz się w rozdziale o [walidacji formularzy |validation].


Wartości domyślne
=================

Elementom formularza często ustawiamy wartości domyślne:

```php
$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);
```

Często przydaje się ustawienie wartości domyślnych wszystkim elementom naraz, na przykład wtedy, gdy formularz służy do edycji rekordów. Wczytamy rekord z bazy danych i ustawimy jego wartości jako domyślne:

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

Wywołaj `setDefaults()` po zdefiniowaniu elementów.

Na już wysłanym formularzu `setDefaults()` nie ma efektu: nie nadpisze tego, co użytkownik wypełnił, więc bezpiecznie możesz je wywoływać bezwarunkowo w fabryce formularza. Jeśli potrzebujesz wymusić wartości także po wysłaniu, użyj zamiast tego `setValues()`.


Renderowanie formularza
=======================

Domyślnie formularz renderowany jest jako tabela. Poszczególne elementy spełniają podstawowe zasady dostępności: wszystkie etykiety generowane są jako elementy `<label>` i powiązane z odpowiednimi elementami formularza. Kliknięcie w etykietę automatycznie ustawia kursor w polu formularza.

Każdemu elementowi możemy ustawić dowolne atrybuty HTML. Dodajmy na przykład placeholder:

```php
$form->addInteger('age', 'Wiek:')
	->setHtmlAttribute('placeholder', 'Podaj wiek');
```

Sposobów renderowania formularza jest mnóstwo, dlatego renderowaniu poświęcony jest [osobny rozdział |rendering].


Renderowanie z Latte
--------------------

Jeśli masz pod ręką system szablonów [Latte |latte:], możesz pozwolić mu wyrenderować formularz i zyskać pełną kontrolę nad wynikowym HTML-em. Tworzysz silnik, rejestrujesz rozszerzenie formularzy i przekazujesz formularz do szablonu jako zmienną:

```php
$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);
```

W szablonie pracujesz potem z formularzem przez zmienną `$form` i tagi takie jak `{input}`, `{label}` czy `n:name`. Kompletny przykład wraz z szablonem znajdziesz w katalogu [examples |https://github.com/nette/forms/tree/master/examples] (pliki `latte.php` i `latte/`). Poszczególne tagi opisuje rozdział o [renderowaniu |rendering].


Mapowanie na klasy
==================

Wróćmy do przetwarzania danych formularza. Metoda `getValues()` zwróciła wysłane dane jako obiekt `ArrayHash`. Ponieważ jest to klasa generyczna, podobna do `stdClass`, brakuje nam przy pracy z nią pewnych wygód, na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Dałoby się to rozwiązać, mając dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.:

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

Alternatywnie możesz użyć konstruktora:

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

Właściwości klasy danych mogą być również enumami i zostaną automatycznie zmapowane. .{data-version:3.2.4}

Jak powiedzieć Nette, żeby zwracało dane jako obiekty tej klasy? Prościej, niż myślisz. Wystarczy podać jako parametr nazwę klasy albo obiekt do zhydratowania:

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

Jako parametr możesz podać także `'array'`, a dane zostaną zwrócone jako tablica.

Jeśli formularze składają się z wielopoziomowej struktury złożonej z kontenerów, utwórz dla każdego osobną klasę:

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

Mapowanie wie wtedy z typu właściwości `$person`, że ma zmapować kontener na klasę `PersonFormData`. Gdyby właściwość miała zawierać tablicę kontenerów, podaj typ `array` i przekaż klasę do zmapowania bezpośrednio kontenerowi:

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

Propozycję klasy danych formularza możesz wygenerować metodą `Nette\Forms\Blueprint::dataClass($form)`, która wypisze ją na stronie w przeglądarce. Następnie wystarczy kliknięciem zaznaczyć kod i skopiować go do projektu. .{data-version:3.1.15}


Wiele przycisków wysyłających
=============================

Jeśli formularz ma więcej niż jeden przycisk, zwykle musimy rozróżnić, który z nich został naciśnięty. Tę informację zwraca metoda przycisku `isSubmittedBy()`:

```php
$form->addSubmit('save', 'Zapisz');
$form->addSubmit('delete', 'Usuń');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}
```

Nie pomijaj sprawdzenia `$form->isSuccess()`, weryfikuje ono poprawność danych.

Gdy formularz zostanie wysłany naciśnięciem klawisza <kbd>Enter</kbd>, traktowany jest tak, jakby został wysłany pierwszym przyciskiem.


Ochrona przed podatnościami
===========================

Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o właściwe zabezpieczenie formularzy.

Oprócz ochrony formularzy przed dobrze znanymi podatnościami, takimi jak [Cross-Site Scripting (XSS) |nette:glossary#Cross-Site Scripting (XSS)] i [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery (CSRF)], wykonuje mnóstwo drobnych zabezpieczeń, o których już nie musisz myśleć.

Na przykład odfiltrowuje z inputów wszystkie znaki sterujące i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza są zawsze czyste. Przy selectboxach i radiolistach weryfikuje, czy wybrane pozycje rzeczywiście były wśród oferowanych i czy nie doszło do podrobienia. Wspominaliśmy już, że przy jednoliniowych inputach tekstowych zamienia na spacje znaki końca linii, które mógłby wysłać atakujący. Przy inputach wieloliniowych normalizuje znaki końca linii. I tak dalej.

Nette rozwiązuje za Ciebie zagrożenia bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją.

Wspomniany atak CSRF polega na tym, że atakujący zwabi ofiarę na stronę, która po cichu wykona w przeglądarce ofiary żądanie do serwera, na którym ofiara jest właśnie zalogowana. Serwer uzna, że żądanie zostało wykonane przez ofiarę dobrowolnie. Dlatego Nette odrzuca formularze POST wysłane z obcego origin; za obcą uznaje się nawet inną subdomenę tej samej witryny. Jeśli potrzebujesz zezwolić na wysyłanie z innego origin, wyłącz ochronę:

```php
$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę całkowicie!
```

To jednak wyłącza ochronę dla dowolnego origin. Żeby zezwolić tylko na konkretne, wyłącz ochronę i samodzielnie zweryfikuj nagłówek `Origin` względem własnej listy dozwolonych.

Ochrona opiera się na nagłówku przeglądarki `Sec-Fetch-Site` (Fetch Metadata), który przeglądarka wysyła automatycznie i którego nie da się podrobić nawet przy podatności XSS. Starsze przeglądarki, które tych nagłówków nie wysyłają, kontroli nie przejdą. Szczegółowo opisuje to artykuł [Przeglądarka wreszcie rozwiązuje CSRF |https://blog.nette.org/en/quarter-century-of-csrf].

.[note]
Wcześniejsza ochrona za pomocą tokenu autoryzacyjnego przechowywanego w sesji, aktywowana przez `$form->addProtection()`, nie jest już potrzebna i od wersji 3.3 jest przestarzała.

Tak oto mamy za sobą szybkie wprowadzenie do formularzy w Nette. Po więcej inspiracji zajrzyj do katalogu [examples |https://github.com/nette/forms/tree/master/examples] w dystrybucji.

Formularze używane samodzielnie

Nette Forms dramatycznie upraszczają tworzenie i przetwarzanie formularzy webowych. Możesz ich używać w swoich aplikacjach całkowicie samodzielnie, bez reszty frameworku, co pokazujemy w tym rozdziale.

Jeśli jednak używasz Nette Application i presenterów, jest dla Ciebie osobny przewodnik: formularze w presenterach.

Pierwszy formularz

Zanim zaczniesz, zainstaluj pakiet za pomocą Composera:

composer require nette/forms

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak (pełny kod):

use Nette\Forms\Form;

$form = new Form;
$form->addText('name', 'Imię:');
$form->addPassword('password', 'Hasło:');
$form->addSubmit('send', 'Zarejestruj się');

I wyrenderujmy go bardzo prosto:

$form->render();

Wynik w przeglądarce powinien wyglądać tak:

Formularz jest obiektem klasy Nette\Forms\Form (klasa Nette\Application\UI\Form używana jest w presenterach). Dodaliśmy do niego elementy o nazwach „name“, „password“ oraz przycisk wysyłający.

Teraz ożywmy formularz. Zapytaniem $form->isSuccess() sprawdzimy, czy formularz został wysłany i czy był poprawnie wypełniony. Jeśli tak, wypiszemy dane. Za definicją formularza dopiszemy:

if ($form->isSuccess()) {
	echo 'Formularz został poprawnie wypełniony i wysłany';
	$data = $form->getValues();
	// $data->name zawiera imię
	// $data->password zawiera hasło
	var_dump($data);
}

Metoda getValues() zwraca wysłane dane jako obiekt ArrayHash. Jak to zmienić, pokażemy później. Obiekt $data zawiera klucze name i password z danymi wpisanymi przez użytkownika.

Zwykle wysyłamy dane bezpośrednio do dalszego przetwarzania, którym może być na przykład zapis do bazy danych. Podczas przetwarzania może jednak dojść do błędu, na przykład nazwa użytkownika jest już zajęta. W takim przypadku przekazujemy błąd z powrotem do formularza za pomocą addError() i pozwalamy go wyrenderować ponownie, wraz z komunikatem o błędzie.

$form->addError('Przepraszamy, ta nazwa użytkownika jest już zajęta.');

Po przetworzeniu formularza przekierowujemy na kolejną stronę. Zapobiega to niezamierzonemu ponownemu wysłaniu formularza przyciskiem odśwież albo wstecz czy przez przemieszczanie się po historii przeglądarki.

Domyślnie formularz wysyłany jest metodą POST na tę samą stronę. Jedno i drugie da się zmienić:

$form->setAction('/submit.php');
$form->setMethod('GET');

I to w zasadzie wszystko :-) Mamy działający i doskonale zabezpieczony formularz.

Spróbuj dodać też inne elementy formularza.

Dostęp do elementów

Formularz i jego poszczególne elementy nazywamy komponentami. Tworzą one drzewo komponentów, którego korzeniem jest formularz. Do poszczególnych elementów formularza dostaniesz się tak:

$input = $form->getComponent('name');
// alternatywna składnia: $input = $form['name'];

$button = $form->getComponent('send');
// alternatywna składnia: $button = $form['send'];

Elementy usuwa się za pomocą unset:

unset($form['name']);

Reguły walidacyjne

Padło słowo valid, ale formularz nie ma jeszcze żadnych reguł walidacyjnych. Naprawmy to.

Imię będzie obowiązkowe, więc oznaczymy je metodą setRequired(). Jej argumentem jest tekst komunikatu o błędzie, który wyświetli się, jeśli użytkownik imienia nie wypełni. Jeśli argumentu nie podamy, użyty zostanie domyślny komunikat o błędzie.

$form->addText('name', 'Imię:')
	->setRequired('Podaj imię.');

Spróbuj wysłać formularz bez wypełnionego imienia, a zobaczysz, że pojawi się komunikat o błędzie. Przeglądarka albo serwer odrzuci go, dopóki pola nie wypełnisz.

Jednocześnie systemu nie oszukasz, wpisując do inputu same spacje. Nie ma szans. Nette automatycznie przycina białe znaki z lewej i prawej strony. Wypróbuj to. To coś, co powinieneś zawsze robić z każdym jednoliniowym inputem, ale o czym często się zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i wysłać jako imię ciąg wieloliniowy. Nawet tutaj Nette nie da się nabrać, a złamania linii zostaną zamienione na spacje.)

Formularz jest zawsze walidowany po stronie serwera, ale generowana jest też walidacja w JavaScripcie. Ta działa błyskawicznie i użytkownik dowiaduje się o błędzie natychmiast, bez potrzeby wysyłania formularza na serwer. Zajmuje się tym skrypt netteForms.js. Wstaw go na stronę:

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

Jeśli zajrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette wstawia obowiązkowe elementy do elementów z klasą CSS required. Spróbuj dodać do szablonu poniższy arkusz stylów, a etykieta „Imię“ stanie się czerwona. Elegancko oznaczysz w ten sposób użytkownikom elementy obowiązkowe:

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

Kolejne reguły walidacyjne dodajemy metodą addRule(). Pierwszym parametrem jest reguła, drugim znów tekst komunikatu o błędzie, a dalej może następować opcjonalny argument reguły walidacyjnej. Co to znaczy?

Rozszerzmy formularz o nowe, opcjonalne pole „wiek“, które musi być liczbą całkowitą (addInteger()) i mieścić się w dozwolonym przedziale ($form::Range). I tutaj wykorzystamy trzeci parametr metody addRule(), którym przekażemy walidatorowi wymagany przedział jako parę [min, max]:

$form->addInteger('age', 'Wiek:')
	->addRule($form::Range, 'Wiek musi mieścić się między 18 a 120.', [18, 120]);

Jeśli użytkownik pola nie wypełni, reguły walidacyjne nie będą sprawdzane, bo element jest opcjonalny.

Powstaje tu miejsce na drobny refaktoring. W komunikacie o błędzie i w trzecim parametrze liczby są zduplikowane, co nie jest idealne. Gdybyśmy tworzyli formularze wielojęzyczne i komunikat zawierający liczby byłby przetłumaczony na kilka języków, zmiana wartości stałaby się trudna. Z tego powodu można użyć zastępników %d, a Nette wartości uzupełni:

	->addRule($form::Range, 'Wiek musi mieścić się między %d a %d lat.', [18, 120]);

Wróćmy do elementu password, uczyńmy go również obowiązkowym i sprawdźmy jeszcze minimalną długość hasła ($form::MinLength), znów z użyciem zastępnika w komunikacie:

$form->addPassword('password', 'Hasło:')
	->setRequired('Wybierz hasło')
	->addRule($form::MinLength, 'Hasło musi mieć co najmniej %d znaków', 8);

Dodajmy do formularza jeszcze pole passwordVerify, w którym użytkownik wpisze hasło ponownie dla kontroli. Za pomocą reguł walidacyjnych sprawdzimy, czy oba hasła są takie same ($form::Equal). Jako parametr podamy odwołanie do pierwszego hasła za pomocą nawiasów kwadratowych:

$form->addPassword('passwordVerify', 'Hasło ponownie:')
	->setRequired('Wpisz hasło jeszcze raz dla kontroli')
	->addRule($form::Equal, 'Hasła nie są zgodne', $form['password'])
	->setOmitted();

Za pomocą setOmitted() oznaczyliśmy element, którego wartość właściwie nas nie interesuje i który istnieje tylko na potrzeby walidacji. Jego wartość nie jest przekazywana do $data.

Tym samym mamy w pełni działający formularz z walidacją w PHP i JavaScripcie. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, na ich podstawie pokazywać i ukrywać części strony itd. Wszystkiego dowiesz się w rozdziale o walidacji formularzy.

Wartości domyślne

Elementom formularza często ustawiamy wartości domyślne:

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

Często przydaje się ustawienie wartości domyślnych wszystkim elementom naraz, na przykład wtedy, gdy formularz służy do edycji rekordów. Wczytamy rekord z bazy danych i ustawimy jego wartości jako domyślne:

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

Wywołaj setDefaults() po zdefiniowaniu elementów.

Na już wysłanym formularzu setDefaults() nie ma efektu: nie nadpisze tego, co użytkownik wypełnił, więc bezpiecznie możesz je wywoływać bezwarunkowo w fabryce formularza. Jeśli potrzebujesz wymusić wartości także po wysłaniu, użyj zamiast tego setValues().

Renderowanie formularza

Domyślnie formularz renderowany jest jako tabela. Poszczególne elementy spełniają podstawowe zasady dostępności: wszystkie etykiety generowane są jako elementy <label> i powiązane z odpowiednimi elementami formularza. Kliknięcie w etykietę automatycznie ustawia kursor w polu formularza.

Każdemu elementowi możemy ustawić dowolne atrybuty HTML. Dodajmy na przykład placeholder:

$form->addInteger('age', 'Wiek:')
	->setHtmlAttribute('placeholder', 'Podaj wiek');

Sposobów renderowania formularza jest mnóstwo, dlatego renderowaniu poświęcony jest osobny rozdział.

Renderowanie z Latte

Jeśli masz pod ręką system szablonów Latte, możesz pozwolić mu wyrenderować formularz i zyskać pełną kontrolę nad wynikowym HTML-em. Tworzysz silnik, rejestrujesz rozszerzenie formularzy i przekazujesz formularz do szablonu jako zmienną:

$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);

W szablonie pracujesz potem z formularzem przez zmienną $form i tagi takie jak {input}, {label} czy n:name. Kompletny przykład wraz z szablonem znajdziesz w katalogu examples (pliki latte.php i latte/). Poszczególne tagi opisuje rozdział o renderowaniu.

Mapowanie na klasy

Wróćmy do przetwarzania danych formularza. Metoda getValues() zwróciła wysłane dane jako obiekt ArrayHash. Ponieważ jest to klasa generyczna, podobna do stdClass, brakuje nam przy pracy z nią pewnych wygód, na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Dałoby się to rozwiązać, mając dla każdego formularza konkretną klasę, której właściwości reprezentują poszczególne elementy. Np.:

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

Alternatywnie możesz użyć konstruktora:

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

Właściwości klasy danych mogą być również enumami i zostaną automatycznie zmapowane.

Jak powiedzieć Nette, żeby zwracało dane jako obiekty tej klasy? Prościej, niż myślisz. Wystarczy podać jako parametr nazwę klasy albo obiekt do zhydratowania:

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

Jako parametr możesz podać także 'array', a dane zostaną zwrócone jako tablica.

Jeśli formularze składają się z wielopoziomowej struktury złożonej z kontenerów, utwórz dla każdego osobną klasę:

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

Mapowanie wie wtedy z typu właściwości $person, że ma zmapować kontener na klasę PersonFormData. Gdyby właściwość miała zawierać tablicę kontenerów, podaj typ array i przekaż klasę do zmapowania bezpośrednio kontenerowi:

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

Propozycję klasy danych formularza możesz wygenerować metodą Nette\Forms\Blueprint::dataClass($form), która wypisze ją na stronie w przeglądarce. Następnie wystarczy kliknięciem zaznaczyć kod i skopiować go do projektu.

Wiele przycisków wysyłających

Jeśli formularz ma więcej niż jeden przycisk, zwykle musimy rozróżnić, który z nich został naciśnięty. Tę informację zwraca metoda przycisku isSubmittedBy():

$form->addSubmit('save', 'Zapisz');
$form->addSubmit('delete', 'Usuń');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

Nie pomijaj sprawdzenia $form->isSuccess(), weryfikuje ono poprawność danych.

Gdy formularz zostanie wysłany naciśnięciem klawisza Enter, traktowany jest tak, jakby został wysłany pierwszym przyciskiem.

Ochrona przed podatnościami

Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o właściwe zabezpieczenie formularzy.

Oprócz ochrony formularzy przed dobrze znanymi podatnościami, takimi jak Cross-Site Scripting (XSS)Cross-Site Request Forgery (CSRF), wykonuje mnóstwo drobnych zabezpieczeń, o których już nie musisz myśleć.

Na przykład odfiltrowuje z inputów wszystkie znaki sterujące i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza są zawsze czyste. Przy selectboxach i radiolistach weryfikuje, czy wybrane pozycje rzeczywiście były wśród oferowanych i czy nie doszło do podrobienia. Wspominaliśmy już, że przy jednoliniowych inputach tekstowych zamienia na spacje znaki końca linii, które mógłby wysłać atakujący. Przy inputach wieloliniowych normalizuje znaki końca linii. I tak dalej.

Nette rozwiązuje za Ciebie zagrożenia bezpieczeństwa, o których wielu programistów nawet nie wie, że istnieją.

Wspomniany atak CSRF polega na tym, że atakujący zwabi ofiarę na stronę, która po cichu wykona w przeglądarce ofiary żądanie do serwera, na którym ofiara jest właśnie zalogowana. Serwer uzna, że żądanie zostało wykonane przez ofiarę dobrowolnie. Dlatego Nette odrzuca formularze POST wysłane z obcego origin; za obcą uznaje się nawet inną subdomenę tej samej witryny. Jeśli potrzebujesz zezwolić na wysyłanie z innego origin, wyłącz ochronę:

$form->allowCrossOrigin(); // UWAGA! Wyłącza ochronę całkowicie!

To jednak wyłącza ochronę dla dowolnego origin. Żeby zezwolić tylko na konkretne, wyłącz ochronę i samodzielnie zweryfikuj nagłówek Origin względem własnej listy dozwolonych.

Ochrona opiera się na nagłówku przeglądarki Sec-Fetch-Site (Fetch Metadata), który przeglądarka wysyła automatycznie i którego nie da się podrobić nawet przy podatności XSS. Starsze przeglądarki, które tych nagłówków nie wysyłają, kontroli nie przejdą. Szczegółowo opisuje to artykuł Przeglądarka wreszcie rozwiązuje CSRF.

Wcześniejsza ochrona za pomocą tokenu autoryzacyjnego przechowywanego w sesji, aktywowana przez $form->addProtection(), nie jest już potrzebna i od wersji 3.3 jest przestarzała.

Tak oto mamy za sobą szybkie wprowadzenie do formularzy w Nette. Po więcej inspiracji zajrzyj do katalogu examples w dystrybucji.