Formularze używane samodzielnie

Nette Forms znacznie ułatwiają tworzenie i przetwarzanie formularzy internetowych. Możesz ich używać w swoich aplikacjach całkowicie samodzielnie, bez reszty frameworka, co pokażemy w tym rozdziale.

Jeśli jednak używasz Nette Application i prezenterów, przeznaczony jest dla Ciebie przewodnik dotyczący użycia w prezenterach.

Pierwszy formularz

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie następujący (cały kod):

use Nette\Forms\Form;

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

Bardzo łatwo go wyrenderujemy:

$form->render();

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

Formularz jest obiektem klasy Nette\Forms\Form (klasa Nette\Application\UI\Form jest używana w prezenterach). Dodaliśmy do niego tzw. elementy: imię, hasło i przycisk wysyłający.

A teraz ożywimy formularz. Pytając $form->isSuccess() dowiemy się, czy formularz został wysłany i czy został wypełniony poprawnie. Jeśli tak, wypiszemy dane. Za definicją formularza dopiszemy więc:

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 przesłane dane w postaci obiektu ArrayHash. Jak to zmienić, pokażemy później. Obiekt $data zawiera klucze name i password z danymi, które wypełnił użytkownik.

Zwykle dane od razu wysyłamy do dalszego przetwarzania, co może być na przykład wstawienie do bazy danych. Podczas przetwarzania może jednak pojawić się błąd, na przykład nazwa użytkownika jest już zajęta. W takim przypadku błąd przekazujemy z powrotem do formularza za pomocą addError() i pozwalamy mu wyrenderować się ponownie, wraz z komunikatem o błędzie.

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

Po przetworzeniu formularza przekierowujemy na następną stronę. Zapobiega to niechcianemu ponownemu wysłaniu formularza przyciskiem odśwież, wstecz lub poruszaniem się w historii przeglądarki.

Formularz standardowo wysyłany jest metodą POST i to na tę samą stronę. Oba te ustawienia można zmienić:

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

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

Spróbuj dodać także inne elementy formularza.

Dostęp do elementów

Formularz i jego poszczególne elementy nazywamy komponentami. Tworzą one drzewo komponentów, gdzie korzeniem jest właśnie formularz. Do poszczególnych elementów formularza dostaniemy się w ten sposób:

$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 walidacji

Padło tu słowo poprawny, ale formularz na razie nie ma żadnych reguł walidacji. Naprawmy to.

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

$form->addText('name', 'Imię:')
	->setRequired('Proszę podać imię');

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

Jednocześnie systemu nie oszukasz, wpisując w pole na przykład same spacje. Nic z tego. Nette automatycznie usuwa spacje z lewej i prawej strony. Wypróbuj to. To jest rzecz, którą powinieneś zawsze robić z każdym jednoliniowym inputem, ale często się o tym zapomina. Nette robi to automatycznie. (Możesz spróbować oszukać formularz i jako imię wysłać wieloliniowy ciąg znaków. Nawet tutaj Nette nie da się zwieść i zamieni znaki nowej linii na spacje.)

Formularz zawsze jest walidowany po stronie serwera, ale generowana jest również walidacja JavaScriptowa, która przebiega błyskawicznie, a użytkownik dowiaduje się o błędzie natychmiast, bez konieczności wysyłania formularza na serwer. Za to odpowiada skrypt netteForms.js. Wstaw go na stronę:

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

Jeśli spojrzysz do kodu źródłowego strony z formularzem, możesz zauważyć, że Nette obowiązkowe elementy wstawia do elementów z klasą CSS required. Spróbuj dodać do szablonu następujący arkusz stylów, a etykieta „Imię” będzie czerwona. W ten sposób elegancko zaznaczymy użytkownikom obowiązkowe elementy:

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

Kolejne reguły walidacji dodamy metodą addRule(). Pierwszy parametr to reguła, drugi to ponownie tekst komunikatu o błędzie, a może jeszcze nastąpić argument reguły walidacji. Co to oznacza?

Formularz rozszerzymy o nowe, nieobowiązkowe pole „wiek”, które musi być liczbą całkowitą (addInteger()) i dodatkowo w dozwolonym zakresie ($form::Range). I tutaj właśnie wykorzystamy trzeci parametr metody addRule(), którym przekażemy walidatorowi wymagany zakres jako parę [od, do]:

$form->addInteger('age', 'Wiek:')
	->addRule($form::Range, 'Wiek musi być od 18 do 120', [18, 120]);

Jeśli użytkownik nie wypełni pola, reguły walidacji nie będą sprawdzane, ponieważ element jest nieobowiązkowy.

Tutaj pojawia się miejsce na mały refactoring. W komunikacie o błędzie i w trzecim parametrze liczby są podane podwójnie, co nie jest idealne. Gdybyśmy tworzyli formularze wielojęzyczne, a komunikat zawierający liczby byłby przetłumaczony na wiele języków, utrudniłoby to ewentualną zmianę wartości. Z tego powodu można użyć symboli zastępczych %d, a Nette uzupełni wartości:

	->addRule($form::Range, 'Wiek musi wynosić od %d do %d lat', [18, 120]);

Wróćmy do elementu password, który również uczynimy obowiązkowym i jeszcze sprawdzimy minimalną długość hasła ($form::MinLength), ponownie wykorzystując symbol zastępczy:

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

Dodamy do formularza jeszcze pole passwordVerify, gdzie użytkownik wprowadzi hasło jeszcze raz, dla kontroli. Za pomocą reguł walidacji sprawdzimy, czy oba hasła są takie same ($form::Equal). A jako parametr podamy odwołanie do pierwszego hasła za pomocą nawiasów kwadratowych:

$form->addPassword('passwordVerify', 'Hasło do weryfikacji:')
	->setRequired('Proszę wprowadzić hasło ponownie w celu weryfikacji')
	->addRule($form::Equal, 'Hasła nie pasują', $form['password'])
	->setOmitted();

Za pomocą setOmitted() oznaczyliśmy element, którego wartość nas właściwie nie obchodzi i który istnieje tylko ze względu na walidację. Wartość nie zostanie przekazana do $data.

Tym samym mamy gotowy, w pełni funkcjonalny formularz z walidacją w PHP i JavaScript. Możliwości walidacyjne Nette są znacznie szersze, można tworzyć warunki, pozwalać na ich podstawie wyświetlać i ukrywać części strony itp. Wszystkiego dowiesz się w rozdziale o walidacji formularzy.

Wartości domyślne

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

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

Często przydaje się ustawienie wartości domyślnych dla wszystkich elementów jednocześnie. Na przykład, gdy formularz służy do edycji rekordów. Odczytujemy rekord z bazy danych i ustawiamy wartości domyślne:

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

Wywołuj setDefaults() dopiero po zdefiniowaniu elementów.

Renderowanie formularza

Standardowo formularz renderuje się jako tabela. Poszczególne elementy spełniają podstawową zasadę dostępności – wszystkie etykiety są zapisane jako <label> i powiązane z odpowiednim elementem formularza. Po kliknięciu na etykietę kursor automatycznie pojawia się w polu formularza.

Każdemu elementowi możemy ustawiać dowolne atrybuty HTML. Na przykład dodać placeholder:

$form->addInteger('age', 'Wiek:')
	->setHtmlAttribute('placeholder', 'Proszę podać wiek');

Sposobów na wyrenderowanie formularza jest naprawdę wiele, dlatego poświęcono temu osobny rozdział o renderowaniu.

Mapowanie na klasy

Wróćmy do przetwarzania danych formularza. Metoda getValues() zwracała nam przesłane dane jako obiekt ArrayHash. Ponieważ jest to klasa generyczna, coś w rodzaju stdClass, podczas pracy z nią zabraknie nam pewnego komfortu, jak na przykład podpowiadania właściwości w edytorach czy statycznej analizy kodu. Można by to rozwiązać, tworzą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 wykorzystać konstruktor:

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

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

Jak powiedzieć Nette, aby zwracał nam dane jako obiekty tej klasy? Łatwiej niż myślisz. Wystarczy tylko nazwę klasy lub obiekt do hydratacji podać jako parametr:

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

Jako parametr można podać również 'array' i wtedy dane zwróci jako tablicę.

Jeśli formularze tworzą 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 następnie z typu właściwości $person rozpozna, że ma kontener mapować na klasę PersonFormData. Jeśli właściwość zawierałaby tablicę kontenerów, podaj typ array i klasę do mapowania przekaż bezpośrednio kontenerowi:

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

Projekt klasy danych formularza możesz wygenerować za pomocą metody Nette\Forms\Blueprint::dataClass($form), która wypisze go na stronie przeglądarki. Kod wystarczy następnie kliknięciem zaznaczyć i skopiować do projektu.

Wiele przycisków

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

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

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

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

Nie pomijaj zapytania $form->isSuccess(), sprawdzisz w ten sposób poprawność danych.

Gdy formularz zostanie wysłany przyciskiem Enter, traktuje się to tak, jakby został wysłany pierwszym przyciskiem.

Ochrona przed lukami w zabezpieczeniach

Nette Framework kładzie duży nacisk na bezpieczeństwo i dlatego skrupulatnie dba o dobre zabezpieczenie formularzy.

Oprócz tego, że formularze chronią przed atakiem Cross Site Scripting (XSS)Cross-Site Request Forgery (CSRF), wykonuje wiele drobnych zabezpieczeń, o których Ty już nie musisz myśleć.

Na przykład odfiltrowuje ze wejść wszystkie znaki kontrolne i sprawdza poprawność kodowania UTF-8, dzięki czemu dane z formularza zawsze będą czyste. W przypadku select boxów i list radio sprawdza, czy wybrane pozycje rzeczywiście pochodziły z oferowanych i czy nie doszło do fałszerstwa. Już wspominaliśmy, że w przypadku jednoliniowych wejść tekstowych usuwa znaki końca linii, które mógł tam wysłać atakujący. W przypadku wejść wieloliniowych z kolei normalizuje znaki końca linii. I tak dalej.

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

Wspomniany atak CSRF polega na tym, że atakujący zwabia ofiarę na stronę, która niepostrzeżenie w przeglądarce ofiary wykonuje żądanie do serwera, na którym ofiara jest zalogowana, a serwer sądzi, że żądanie wykonała ofiara z własnej woli. Dlatego Nette zapobiega wysyłaniu formularza POST z innej domeny. Jeśli z jakiegoś powodu chcesz wyłączyć ochronę i pozwolić na wysyłanie formularza z innej domeny, użyj:

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

Ta ochrona wykorzystuje ciasteczko SameSite o nazwie _nss. Twórz zatem obiekt formularza jeszcze przed wysłaniem pierwszego wyjścia, aby można było wysłać ciasteczko.

Ochrona za pomocą ciasteczka SameSite może nie być w 100% niezawodna, dlatego warto włączyć jeszcze ochronę za pomocą tokenu:

$form->addProtection();

Zalecamy w ten sposób chronić formularze w części administracyjnej strony, które zmieniają wrażliwe dane w aplikacji. Framework broni się przed atakiem CSRF, generując i weryfikując token autoryzacyjny, który jest przechowywany w sesji. Dlatego konieczne jest, aby przed wyświetleniem formularza sesja była otwarta. W części administracyjnej strony zazwyczaj sesja jest już uruchomiona ze względu na logowanie użytkownika. W przeciwnym razie uruchom sesję metodą Nette\Http\Session::start().

Tak, mamy za sobą szybkie wprowadzenie do formularzy w Nette. Spróbuj jeszcze zajrzeć do katalogu examples w dystrybucji, gdzie znajdziesz więcej inspiracji.

wersja: 4.0