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