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) i 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.