Nette Documentation Preview

syntax
Formularze w presenterach
*************************

.[perex]
Nette Forms znacząco upraszczają tworzenie i przetwarzanie formularzy webowych. W tym rozdziale dowiesz się, jak używać formularzy wewnątrz presenterów.

Jeśli interesuje Cię użycie całkowicie samodzielne, bez reszty frameworku, jest dla Ciebie przewodnik po [użyciu samodzielnym|standalone].


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

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak:

```php
use Nette\Application\UI\Form;

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

a w przeglądarce wyświetli się tak:

[* form-en.webp *]

Formularz w presenterze to obiekt klasy `Nette\Application\UI\Form`, jego poprzednik `Nette\Forms\Form` jest przeznaczony do użytku samodzielnego. Dodaliśmy elementy o nazwach name i password oraz przycisk wysyłający. Na koniec linia `$form->onSuccess` mówi, że po wysłaniu i udanej walidacji ma zostać wywołana metoda `$this->formSucceeded()`.

Z perspektywy presentera formularz jest zwykłym komponentem. Dlatego traktuje się go jak komponent i włącza do presentera za pomocą [metody fabrykującej |application:components#Metody fabryczne]. Będzie to wyglądać tak:

```php .{file:app/Presentation/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', 'Imię:');
		$form->addPassword('password', 'Hasło:');
		$form->addSubmit('send', 'Zarejestruj się');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

	private function formSucceeded(Form $form, $data): void
	{
		// tutaj przetworzymy dane wysłane formularzem
		// $data->name zawiera imię
		// $data->password zawiera hasło
		$this->flashMessage('Rejestracja przebiegła pomyślnie.');
		$this->redirect('Home:');
	}
}
```

A w szablonie formularz renderujemy tagiem `{control}`:

```latte .{file:app/Presentation/Home/default.latte}
<h1>Rejestracja</h1>

{control registrationForm}
```

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

Teraz pewnie myślisz, że poszło to zbyt szybko, i zastanawiasz się, jak to możliwe, że wywołuje się metoda `formSucceeded()` i jakie parametry dostaje. Tak, masz rację, to zasługuje na wyjaśnienie.

Nette wprowadza odświeżający mechanizm, który nazywamy [stylem hollywoodzkim |application:components#Styl hollywoodzki]. Zamiast tego, żebyś jako programista musiał ciągle pytać, czy coś się stało ("czy formularz został wysłany?", "czy został wysłany poprawnie?", "czy nie został podrobiony?"), mówisz frameworkowi "kiedy formularz będzie poprawnie wypełniony, wywołaj tę metodę" i dalszą pracę zostawiasz jemu. Jeśli programujesz w JavaScripcie, ten styl programowania znasz doskonale. Piszesz funkcje, które są wywoływane, gdy nastąpi określone [zdarzenie |nette:glossary#Zdarzenia]. A język przekazuje im odpowiednie argumenty.

Dokładnie tak zbudowany jest powyższy kod presentera. Tablica `$form->onSuccess` reprezentuje listę callbacków PHP, które Nette wywoła w momencie, gdy formularz zostanie wysłany i poprawnie wypełniony (czyli będzie valid). W ramach [cyklu życia presentera |application:presenters#Cykl życia presentera] jest to tak zwany sygnał, więc wywołują się po metodzie `action*`, a przed metodą `render*`. I każdemu callbackowi przekazuje jako pierwszy parametr sam formularz, a jako drugi wysłane dane w postaci obiektu [ArrayHash |utils:arrays#ArrayHash] (albo stdClass, albo własnej klasy). Pierwszy parametr możesz pominąć, jeśli obiekt formularza nie jest Ci potrzebny. Drugi parametr potrafi być sprytniejszy, ale o tym [później |#Mapowanie na klasy].

Obiekt `$data` zawiera właściwości `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.');
```

Oprócz `onSuccess` istnieje jeszcze `onSubmit`: callbacki wywoływane są zawsze po wysłaniu formularza, nawet jeśli nie jest poprawnie wypełniony. Oraz `onError`: callbacki wywoływane są tylko wtedy, gdy wysłanie nie jest poprawne. Wywołają się nawet wtedy, gdy unieważnimy formularz w `onSuccess` za pomocą `addError()`.

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

Jeśli formularz jest wysyłany przez AJAX, zwykle zamiast przekierowania przerysowujesz [snippet |application:ajax] z ponownie wyrenderowanym formularzem.

Spróbuj dodać kolejne [elementy formularza|controls].


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

Formularz jest komponentem presentera, w naszym przypadku nazwanym `registrationForm` (po nazwie metody fabrykującej `createComponentRegistrationForm`), więc gdziekolwiek w presenterze dostaniesz się do formularza za pomocą:

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

Poszczególne elementy formularza również są komponentami, więc dostaniesz się do nich w ten sam sposób:

```php
$input = $form->getComponent('name'); // albo $input = $form['name'];
$button = $form->getComponent('send'); // albo $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 argument pominiemy, użyty zostanie domyślny komunikat o błędzie.

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

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

Jednocześnie systemu nie oszukasz, wpisując do pola na przykład 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, która 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 do szablonu layoutu:

```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 otacza obowiązkowe elementy elementami 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 pola 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ć argument reguły walidacyjnej. Co to znaczy?

Rozszerzmy formularz o nowe, opcjonalne pole "wiek", które musi być liczbą całkowitą (`addInteger()`) i w dodatku z dozwolonego przedziału (`$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 argument 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 literówki')
	->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 wartości 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 stron: wszystkie etykiety zapisane 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 naprawdę mnóstwo, dlatego poświęcony jest temu [osobny rozdział o renderowaniu|rendering].


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

Wróćmy do metody `formSucceeded()`, która w drugim parametrze `$data` otrzymuje wysłane dane jako obiekt `ArrayHash` (albo `stdClass`). 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ć klasę jako typ parametru `$data` w metodzie obsługującej:

```php
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data jest instancją RegistrationFormData
	$name = $data->name;
	// ...
}
```

Jako typ możesz podać także `array`, a wtedy dane zostaną przekazane jako tablica.

Podobnie możesz użyć metody `getValues()`, przekazując jej jako parametr nazwę klasy albo obiekt do zhydratowania:

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

Jeśli potrzebujesz odczytać wartości przed walidacją formularza, typowo wewnątrz handlera `onValidate`, użyj zamiast tego metody `getUntrustedValues()`. Przyjmuje te same parametry co `getValues()`, ale zwraca wysłane wartości bez gwarancji, że przeszły walidację.

Jeśli formularze mają wielopoziomową strukturę złożoną 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 wywnioskuje 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. Dla każdego przycisku możemy utworzyć osobną funkcję obsługującą. Ustawimy ją jako handler [zdarzenia |nette:glossary#Zdarzenia] `onClick`:

```php
$form->addSubmit('save', 'Zapisz')
	->onClick[] = $this->saveButtonPressed(...);

$form->addSubmit('delete', 'Usuń')
	->onClick[] = $this->deleteButtonPressed(...);
```

.{data-version:3.3.0}
Handler można też przekazać przyciskowi bezpośrednio jako trzeci argument metody `addSubmit()`.

Handlery te wywoływane są tylko wtedy, gdy formularz jest poprawnie wypełniony (chyba że dla przycisku wyłączono walidację), tak samo jak zdarzenie `onSuccess`. Różnica polega na tym, że jako pierwszy parametr może być przekazany zamiast formularza obiekt przycisku wysyłającego, zależnie od tego, jaki typ podasz:

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

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


Zdarzenie onAnchor
==================

Gdy budujesz formularz w metodzie fabrykującej (jak `createComponentRegistrationForm`), nie wie on jeszcze, czy został wysłany ani z jakimi danymi. Są jednak przypadki, gdy potrzebujemy znać wysłane wartości, na przykład gdy od nich zależy wygląd formularza albo gdy są potrzebne dla zależnych selectboxów itd.

Możesz więc sprawić, żeby kod budujący formularz był wywoływany dopiero wtedy, gdy formularz jest "zakotwiczony", czyli już połączony z presenterem i znający swoje wysłane dane. Taki kod umieść w tablicy `$onAnchor`:

```php
$country = $form->addSelect('country', 'Kraj:', $this->model->getCountries());
$city = $form->addSelect('city', 'Miasto:');

$form->onAnchor[] = function () use ($country, $city) {
	// ta funkcja zostanie wywołana, gdy formularz będzie znał dane, z którymi został wysłany
	// możesz więc użyć metody getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};
```


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

Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o bezpieczeństwo formularzy. Robi to całkowicie transparentnie i nie wymaga żadnego ręcznego ustawiania.

Oprócz ochrony formularzy przed atakami 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 zalogowana. Serwer uzna wtedy, ż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. Dla starszych przeglądarek bez ich wsparcia stosowany jest zapasowy cookie SameSite, które aplikacja Nette ustawia automatycznie. 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.


Użycie jednego formularza w wielu presenterach
==============================================

Jeśli potrzebujesz użyć tego samego formularza w wielu presenterach, zalecamy utworzenie dla niego fabryki, którą następnie wstrzykniesz do presenterów. Odpowiednim miejscem dla takiej klasy jest na przykład katalog `app/Forms`.

Klasa fabryki może wyglądać tak:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Imię:');
		$form->addSubmit('send', 'Zaloguj się');
		return $form;
	}
}
```

O klasę produkującą formularz poprosimy w metodzie fabrykującej komponent w presenterze:

```php
public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// możemy formularz zmienić, tutaj na przykład zmieniamy etykietę na przycisku
	$form['send']->setCaption('Kontynuuj');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // i dodajemy handler
	return $form;
}
```

Handler przetwarzający formularz może dostarczyć również sama fabryka:

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Imię:');
		$form->addSubmit('send', 'Zaloguj się');
		$form->onSuccess[] = function (Form $form, $data): void {
			// tutaj przetwarzamy wysłany formularz
		};
		return $form;
	}
}
```

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 w presenterach

Nette Forms znacząco upraszczają tworzenie i przetwarzanie formularzy webowych. W tym rozdziale dowiesz się, jak używać formularzy wewnątrz presenterów.

Jeśli interesuje Cię użycie całkowicie samodzielne, bez reszty frameworku, jest dla Ciebie przewodnik po użyciu samodzielnym.

Pierwszy formularz

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądać tak:

use Nette\Application\UI\Form;

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

a w przeglądarce wyświetli się tak:

Formularz w presenterze to obiekt klasy Nette\Application\UI\Form, jego poprzednik Nette\Forms\Form jest przeznaczony do użytku samodzielnego. Dodaliśmy elementy o nazwach name i password oraz przycisk wysyłający. Na koniec linia $form->onSuccess mówi, że po wysłaniu i udanej walidacji ma zostać wywołana metoda $this->formSucceeded().

Z perspektywy presentera formularz jest zwykłym komponentem. Dlatego traktuje się go jak komponent i włącza do presentera za pomocą metody fabrykującej. Będzie to wyglądać tak:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Imię:');
		$form->addPassword('password', 'Hasło:');
		$form->addSubmit('send', 'Zarejestruj się');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

	private function formSucceeded(Form $form, $data): void
	{
		// tutaj przetworzymy dane wysłane formularzem
		// $data->name zawiera imię
		// $data->password zawiera hasło
		$this->flashMessage('Rejestracja przebiegła pomyślnie.');
		$this->redirect('Home:');
	}
}

A w szablonie formularz renderujemy tagiem {control}:

<h1>Rejestracja</h1>

{control registrationForm}

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

Teraz pewnie myślisz, że poszło to zbyt szybko, i zastanawiasz się, jak to możliwe, że wywołuje się metoda formSucceeded() i jakie parametry dostaje. Tak, masz rację, to zasługuje na wyjaśnienie.

Nette wprowadza odświeżający mechanizm, który nazywamy stylem hollywoodzkim. Zamiast tego, żebyś jako programista musiał ciągle pytać, czy coś się stało („czy formularz został wysłany?“, „czy został wysłany poprawnie?“, „czy nie został podrobiony?“), mówisz frameworkowi „kiedy formularz będzie poprawnie wypełniony, wywołaj tę metodę“ i dalszą pracę zostawiasz jemu. Jeśli programujesz w JavaScripcie, ten styl programowania znasz doskonale. Piszesz funkcje, które są wywoływane, gdy nastąpi określone zdarzenie. A język przekazuje im odpowiednie argumenty.

Dokładnie tak zbudowany jest powyższy kod presentera. Tablica $form->onSuccess reprezentuje listę callbacków PHP, które Nette wywoła w momencie, gdy formularz zostanie wysłany i poprawnie wypełniony (czyli będzie valid). W ramach cyklu życia presentera jest to tak zwany sygnał, więc wywołują się po metodzie action*, a przed metodą render*. I każdemu callbackowi przekazuje jako pierwszy parametr sam formularz, a jako drugi wysłane dane w postaci obiektu ArrayHash (albo stdClass, albo własnej klasy). Pierwszy parametr możesz pominąć, jeśli obiekt formularza nie jest Ci potrzebny. Drugi parametr potrafi być sprytniejszy, ale o tym później.

Obiekt $data zawiera właściwości 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.');

Oprócz onSuccess istnieje jeszcze onSubmit: callbacki wywoływane są zawsze po wysłaniu formularza, nawet jeśli nie jest poprawnie wypełniony. Oraz onError: callbacki wywoływane są tylko wtedy, gdy wysłanie nie jest poprawne. Wywołają się nawet wtedy, gdy unieważnimy formularz w onSuccess za pomocą addError().

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

Jeśli formularz jest wysyłany przez AJAX, zwykle zamiast przekierowania przerysowujesz snippet z ponownie wyrenderowanym formularzem.

Spróbuj dodać kolejne elementy formularza.

Dostęp do elementów

Formularz jest komponentem presentera, w naszym przypadku nazwanym registrationForm (po nazwie metody fabrykującej createComponentRegistrationForm), więc gdziekolwiek w presenterze dostaniesz się do formularza za pomocą:

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

Poszczególne elementy formularza również są komponentami, więc dostaniesz się do nich w ten sam sposób:

$input = $form->getComponent('name'); // albo $input = $form['name'];
$button = $form->getComponent('send'); // albo $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 argument pominiemy, użyty zostanie domyślny komunikat o błędzie.

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

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

Jednocześnie systemu nie oszukasz, wpisując do pola na przykład 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, która 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 do szablonu layoutu:

<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 otacza obowiązkowe elementy elementami 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 pola 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ć argument reguły walidacyjnej. Co to znaczy?

Rozszerzmy formularz o nowe, opcjonalne pole „wiek“, które musi być liczbą całkowitą (addInteger()) i w dodatku z dozwolonego przedziału ($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 argument 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 literówki')
	->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 wartości 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 stron: wszystkie etykiety zapisane 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 naprawdę mnóstwo, dlatego poświęcony jest temu osobny rozdział o renderowaniu.

Mapowanie na klasy

Wróćmy do metody formSucceeded(), która w drugim parametrze $data otrzymuje wysłane dane jako obiekt ArrayHash (albo stdClass). 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ć klasę jako typ parametru $data w metodzie obsługującej:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $data jest instancją RegistrationFormData
	$name = $data->name;
	// ...
}

Jako typ możesz podać także array, a wtedy dane zostaną przekazane jako tablica.

Podobnie możesz użyć metody getValues(), przekazując jej jako parametr nazwę klasy albo obiekt do zhydratowania:

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

Jeśli potrzebujesz odczytać wartości przed walidacją formularza, typowo wewnątrz handlera onValidate, użyj zamiast tego metody getUntrustedValues(). Przyjmuje te same parametry co getValues(), ale zwraca wysłane wartości bez gwarancji, że przeszły walidację.

Jeśli formularze mają wielopoziomową strukturę złożoną 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 wywnioskuje 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. Dla każdego przycisku możemy utworzyć osobną funkcję obsługującą. Ustawimy ją jako handler zdarzenia onClick:

$form->addSubmit('save', 'Zapisz')
	->onClick[] = $this->saveButtonPressed(...);

$form->addSubmit('delete', 'Usuń')
	->onClick[] = $this->deleteButtonPressed(...);

Handler można też przekazać przyciskowi bezpośrednio jako trzeci argument metody addSubmit().

Handlery te wywoływane są tylko wtedy, gdy formularz jest poprawnie wypełniony (chyba że dla przycisku wyłączono walidację), tak samo jak zdarzenie onSuccess. Różnica polega na tym, że jako pierwszy parametr może być przekazany zamiast formularza obiekt przycisku wysyłającego, zależnie od tego, jaki typ podasz:

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

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

Zdarzenie onAnchor

Gdy budujesz formularz w metodzie fabrykującej (jak createComponentRegistrationForm), nie wie on jeszcze, czy został wysłany ani z jakimi danymi. Są jednak przypadki, gdy potrzebujemy znać wysłane wartości, na przykład gdy od nich zależy wygląd formularza albo gdy są potrzebne dla zależnych selectboxów itd.

Możesz więc sprawić, żeby kod budujący formularz był wywoływany dopiero wtedy, gdy formularz jest „zakotwiczony“, czyli już połączony z presenterem i znający swoje wysłane dane. Taki kod umieść w tablicy $onAnchor:

$country = $form->addSelect('country', 'Kraj:', $this->model->getCountries());
$city = $form->addSelect('city', 'Miasto:');

$form->onAnchor[] = function () use ($country, $city) {
	// ta funkcja zostanie wywołana, gdy formularz będzie znał dane, z którymi został wysłany
	// możesz więc użyć metody getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Ochrona przed podatnościami

Nette Framework kładzie ogromny nacisk na bezpieczeństwo i dlatego skrupulatnie dba o bezpieczeństwo formularzy. Robi to całkowicie transparentnie i nie wymaga żadnego ręcznego ustawiania.

Oprócz ochrony formularzy przed atakami 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 zalogowana. Serwer uzna wtedy, ż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. Dla starszych przeglądarek bez ich wsparcia stosowany jest zapasowy cookie SameSite, które aplikacja Nette ustawia automatycznie. 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.

Użycie jednego formularza w wielu presenterach

Jeśli potrzebujesz użyć tego samego formularza w wielu presenterach, zalecamy utworzenie dla niego fabryki, którą następnie wstrzykniesz do presenterów. Odpowiednim miejscem dla takiej klasy jest na przykład katalog app/Forms.

Klasa fabryki może wyglądać tak:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Imię:');
		$form->addSubmit('send', 'Zaloguj się');
		return $form;
	}
}

O klasę produkującą formularz poprosimy w metodzie fabrykującej komponent w presenterze:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// możemy formularz zmienić, tutaj na przykład zmieniamy etykietę na przycisku
	$form['send']->setCaption('Kontynuuj');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // i dodajemy handler
	return $form;
}

Handler przetwarzający formularz może dostarczyć również sama fabryka:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Imię:');
		$form->addSubmit('send', 'Zaloguj się');
		$form->onSuccess[] = function (Form $form, $data): void {
			// tutaj przetwarzamy wysłany formularz
		};
		return $form;
	}
}

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