Formy w presenterech

Nette Forms znacznie ułatwiają tworzenie i przetwarzanie formularzy internetowych. W tym rozdziale dowiesz się, jak używać formularzy wewnątrz prezenterów.

Jeśli jesteś zainteresowany używaniem ich całkowicie samodzielnie bez reszty frameworka, jest dla Ciebie samodzielny tutorial.

Pierwszy formularz

Spróbujmy napisać prosty formularz rejestracyjny. Jego kod będzie wyglądał następująco:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Jméno:');
$form->addPassword('password', 'Heslo:');
$form->addSubmit('send', 'Registrovat');
$form->onSuccess[] = [$this, 'formSucceeded'];

i zostanie on wyświetlony w przeglądarce w następujący sposób:

Formularz w prezenterze jest obiektem klasy Nette\Application\UI\Form, jego poprzednik Nette\Forms\Form jest przeznaczony do samodzielnego użytku. Dodałem elementy nazwy, hasła i przycisku submit. Wreszcie, linia z $form->onSuccess mówi, aby wywołać metodę $this->formSucceeded() po złożeniu i pomyślnym zatwierdzeniu.

Z punktu widzenia prezentera formularz jest normalnym komponentem. Dlatego traktujemy go jako komponent i włączamy do prezentera za pomocą metody factory. Będzie to wyglądało 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', 'Jméno:');
		$form->addPassword('password', 'Heslo:');
		$form->addSubmit('send', 'Registrovat');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// tutaj przetwarzamy dane przesłane przez formularz
		// $data->name zawiera jméno
		// $data->password zawiera heslo
		$this->flashMessage('Byl jste úspěšně registrován.');
		$this->redirect('Strona główna:');
	}
}

A w szablonie renderujemy formularz z tagiem {control}:

<h1>Registrace</h1>

{control registrationForm}

I to wszystko :-) Mamy funkcjonalną i doskonale zabezpieczoną formę.

I teraz pewnie myślisz, to było za dużo hrr, zastanawiając się jak to możliwe, że wywołujemy metodę formSucceeded() i jakie są parametry, które ona otrzymuje. Jasne, masz rację, to zasługuje na wyjaśnienie.

Bo Nette wymyśla świeży mechanizm, który nazywamy stylem hollywoodzkim. Zamiast konieczności ciągłego zadawania sobie jako deweloper pytania, czy coś się stało (“czy formularz został przesłany?”, “czy został przesłany poprawnie?” i “czy był manipulowany?”), mówisz frameworkowi “kiedy formularz jest ważnie złożony, wywołaj tę metodę” i pozostaw resztę pracy jemu. Jeśli programujesz w JavaScript, jesteś zaznajomiony z tym stylem programowania. Piszesz funkcje, które są wywoływane w momencie wystąpienia zdarzenia. A język przekazuje im odpowiednie argumenty.

Powyższy kod prezentera jest zbudowany właśnie w ten sposób. Pole $form->onSuccess reprezentuje listę wywołań zwrotnych PHP, które Nette wywoła, gdy formularz zostanie przesłany i wypełniony poprawnie (tzn. jest ważny). W ramach cyklu życia prezentera są one wywoływane po metodzie action*, a przed metodą render*. I dla każdego callbacka przekazuje sam formularz jako pierwszy parametr i przesłane dane w postaci obiektu ArrayHash jako drugi. Możesz pominąć pierwszy parametr, jeśli nie potrzebujesz obiektu formularza. A drugi parametr może być bardziej przebiegły, ale o tym więcej później.

Obiekt $data zawiera klucze name i password z danymi wypełnionymi przez użytkownika. Zazwyczaj od razu wysyłamy dane do dalszej obróbki, którą może być np. wstawienie ich do bazy danych. Podczas przetwarzania może jednak wystąpić błąd, na przykład nazwa użytkownika jest już zajęta. W tym przypadku przekazujemy błąd z powrotem do formularza za pomocą addError() i mamy go renderować ponownie, z komunikatem o błędzie.

$form->addError('Omlouváme se, uživatelské jméno už někdo používá.');

Oprócz onSuccess jest jeszcze onSubmit: callbacki są wywoływane zawsze po przesłaniu formularza, nawet jeśli nie jest on wypełniony poprawnie. I onError: callbacki są wywoływane tylko wtedy, gdy zgłoszenie nie jest ważne. Zostaną one nawet wywołane, jeśli onSuccess lub onSubmit unieważni formularz za pomocą addError().

Po przetworzeniu formularza przekierowujemy na kolejną stronę. Zapobiega to niezamierzonemu ponownemu przesłaniu formularza za pomocą przycisków refresh, back lub historii przeglądarki.

Spróbuj dodać również inne elementy formularza.

Dostęp do elementów

Formularz jest komponentem prezentera, w tym przypadku nazwanym registrationForm (od nazwy metody fabrycznej createComponentRegistrationForm), więc w dowolnym miejscu prezentera można uzyskać dostęp do formularza za pomocą:

$form = $this->getComponent('registrationForm');
// alternativní syntax: $form = $this['registrationForm'];

Komponenty są również elementami formularza, więc możesz uzyskać do nich dostęp w ten sam sposób:

$input = $form->getComponent('name'); // nebo $input = $form['name'];
$button = $form->getComponent('send'); // nebo $button = $form['send']

Elementy są usuwane za pomocą unset:

unset($form['name']);

Zasady walidacji

Słowo valid, ale formularz nie ma jeszcze reguł walidacji. Naprawmy to.

Nazwa będzie obowiązkowa, więc oznaczymy ją metodą setRequired(), której argumentem jest tekst komunikatu o błędzie, który zostanie wyświetlony, jeśli użytkownik nie wypełni nazwy. Jeśli nie podano żadnego argumentu, użyty zostanie domyślny komunikat o błędzie.

$form->addText('name', 'Jméno:')
	->setRequired('Zadejte prosím jméno');

Spróbuj przesłać formularz bez wypełnienia nazwy, a zobaczysz, że pojawi się komunikat o błędzie, a przeglądarka lub serwer odrzuci go, dopóki nie wypełnisz pola.

Jednocześnie nie oszukuj systemu, wpisując w pole np. same spacje. Nie rób tego. Nette automatycznie usuwa zarówno lewe jak i prawe spacje. Spróbuj. Jest to rodzaj rzeczy, którą powinieneś zawsze robić z każdym jednolinijkowym wejściem, ale często się o tym zapomina. Nette robi to automatycznie (możesz spróbować oszukać formularz, aby wysłać ciąg wieloliniowy jako nazwę. Nawet tutaj Nette nie da się oszukać i zmieni podziały linii na spacje).

Formularz jest zawsze walidowany po stronie serwera, ale generuje również walidację JavaScript, która jest wykonywana w mgnieniu oka, a użytkownik dowiaduje się o błędzie natychmiast, bez konieczności wysyłania formularza na serwer. Zajmuje się tym skrypt netteForms.js. Wstaw go do szablonu układu:

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

Jeśli spojrzysz na kod źródłowy strony formularza, możesz zauważyć, że Nette wstawia wymagane elementy z klasą CSS required. Spróbuj dodać następujący arkusz stylów do szablonu, a etykieta “Nazwa” będzie czerwona. To elegancko podkreśli wymagane elementy dla użytkownika:

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

Dodaj więcej reguł walidacji używając metody addRule(). Pierwszy parametr to reguła, drugi to ponownie tekst komunikatu o błędzie, po którym może nastąpić argument reguła walidacji. Co należy przez to rozumieć?

Rozszerzamy formularz o nowe, opcjonalne pole “wiek”, które musi być liczbą całkowitą (addInteger()), a także mieścić się w dozwolonym zakresie ($form::Range). Tutaj używamy trzeciego parametru metody addRule(), aby przekazać wymagany zakres do walidatora jako parę [od, do]:

$form->addInteger('age', 'Věk:')
	->addRule($form::Range, 'Věk musí být od 18 do 120', [18, 120]);

Jeśli użytkownik nie wypełni pola, reguły walidacji nie zostaną sprawdzone, ponieważ element jest opcjonalny.

Jest tu miejsce na drobną refaktoryzację. W komunikacie o błędzie i w trzecim parametrze liczby są zduplikowane, co nie jest idealne. Jeśli tworzyliśmy formularze wielojęzyczne, a wiadomość zawierająca liczby została przetłumaczona na wiele języków, utrudniłoby to zmianę wartości w razie potrzeby. Z tego powodu można użyć znaków zastępczych %d, a Nette wypełni wartości:

	->addRule($form::Range, 'Věk musí být od %d do %d let', [18, 120]);

Wróćmy do elementu password, który również czynimy obowiązkowym, i sprawdźmy minimalną długość hasła ($form::MinLength), ponownie używając znaku wieloznacznego:

$form->addPassword('password', 'Heslo:')
	->setRequired('Zvolte si heslo')
	->addRule($form::MinLength, 'Heslo musí mít alespoň %d znaků', 8);

Dodajmy do formularza pole passwordVerify, w którym użytkownik wpisuje hasło jeszcze raz, w celu weryfikacji. Korzystając z reguł walidacji sprawdzamy, czy oba hasła są takie same ($form::Equal). A jako parametr umieszczamy odwołanie do pierwszego hasła za pomocą nawiasów kwadratowych:

$form->addPassword('passwordVerify', 'Heslo pro kontrolu:')
	->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu')
	->addRule($form::Equal, 'Hesla se neshodují', $form['password'])
	->setOmitted();

Za pomocą setOmitted() zaznaczyliśmy element, którego wartość tak naprawdę nas nie obchodzi i który istnieje tylko ze względu na walidację. Wartość nie jest przekazywana do $data.

W ten sposób mamy w pełni funkcjonalny formularz z walidacją w PHP i JavaScript. Możliwości walidacji Nette są znacznie szersze, możesz tworzyć warunki, mieć części strony wyświetlane i ukryte zgodnie z nimi itp. Możesz dowiedzieć się wszystkiego na ten temat w rozdziale poświęconym walidacji formularzy.

Wartości domyślne

Standardowo ustawiamy wartości domyślne dla elementów formularza:

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

Często przydatne jest ustawienie wartości domyślnych dla wszystkich elementów jednocześnie. Na przykład, gdy formularz jest używany do edycji rekordów. Odczytaj rekord z bazy danych i ustaw wartości domyślne:

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

Po zdefiniowaniu elementów wywołaj setDefaults().

Rendering formularza

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

Dla każdego elementu możemy ustawić dowolne atrybuty HTML. Na przykład dodać placeholder:

$form->addInteger('age', 'Věk:')
	->setHtmlAttribute('placeholder', 'Prosím vyplňte věk');

Istnieje wiele sposobów renderowania formularza, dlatego na temat renderowania jest osobny rozdział.

Odwzorowanie na klasy

Wróćmy do metody formSucceeded(), która w drugim parametrze $data pobiera wysłane dane jako obiekt ArrayHash. Ponieważ jest to klasa generyczna, coś w rodzaju stdClass, zabraknie nam pewnych udogodnień podczas pracy z nią, takich jak uzupełnianie kodu dla właściwości w edytorach czy statyczna analiza kodu. Można to rozwiązać poprzez posiadanie specyficznej klasy dla każdego formularza, której właściwości reprezentują poszczególne kontrolki. Np:

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

Od PHP 8.0 możesz użyć tej eleganckiej notacji, która wykorzystuje konstruktor:

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

Jak powiedzieć Nette, aby zwracała dane jako obiekty tej klasy? Łatwiejsze niż myślisz. Wystarczy określić klasę jako typ parametru $data w metodzie handler:

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

Możesz również określić array jako typ, a następnie przekazać dane jako tablicę.

W podobny sposób można użyć funkcji getValues(), aby przekazać jako parametr nazwę klasy lub obiektu, który ma zostać uwodniony:

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

Jeśli formularze tworzą wielopoziomową strukturę złożoną z kontenerów, utwórz dla każdego z nich 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 powinno mapować kontener do klasy PersonFormData. Jeśli właściwość zawierałaby tablicę kontenerów, określ typ array i przekaż klasę mapowania bezpośrednio do kontenera:

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

Propozycję klasy danych formularza można wygenerować za pomocą metody Nette\Forms\Blueprint::dataClass($form), która wydrukuje ją na stronie przeglądarki. Następnie wystarczy kliknąć, aby wybrać i skopiować kod do swojego projektu.

Więcej przycisków

Jeśli formularz ma więcej niż jeden przycisk, zwykle musimy rozróżnić, który z nich został naciśnięty. Możemy stworzyć niestandardowy handler dla każdego przycisku. Ustawiamy go jako handler dla zdarzenia onClick:

$form->addSubmit('save', 'Uložit')
	->onClick[] = [$this, 'saveButtonPressed'];

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

Te handlery są wywoływane tylko wtedy, gdy formularz jest poprawnie wypełniony, podobnie jak zdarzenie onSuccess. Różnica polega na tym, że przycisk submit może być przekazany jako pierwszy parametr zamiast formularza, w zależności od typu, który określisz:

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

Gdy formularz zostanie przesłany za pomocą przycisku Enter, jest traktowany tak, jakby został przesłany za pomocą pierwszego przycisku.

Zdarzenie onAnchor

Kiedy budujemy formularz w metodzie fabrycznej (takiej jak createComponentRegistrationForm), nie wie on jeszcze, czy został złożony, ani z jakimi danymi. Istnieją jednak przypadki, w których musimy znać przekazane wartości, być może w celu wyprowadzenia następnego formularza formularza lub dla zależnych pól wyboru itp.

Dlatego możesz mieć kod budujący formularz wywoływany tylko wtedy, gdy jest zakotwiczony, czyli jest już połączony z prezenterem i zna jego przesłane dane. Taki kod przekazujemy do pola $onAnchor:

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

$form->onAnchor[] = function () use ($country, $city)
	// ta funkcja zostanie wywołana, gdy formularz będzie wiedział, czy został przesłany i z jakimi danymi
	// więc możesz użyć metody getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Ochrona przed podatnością na zagrożenia

Nette Framework przykłada dużą wagę do bezpieczeństwa i dlatego skrupulatnie dba o to, aby formularze były dobrze zabezpieczone. Robi to w sposób całkowicie przejrzysty i nie wymaga ręcznej konfiguracji.

Oprócz ochrony formularzy przed atakami Cross Site Scripting (XSS)Cross-Site Request Forgery (CSRF), robi wiele małych rzeczy związanych z bezpieczeństwem, o których nie musisz myśleć.

Na przykład filtruje wszystkie znaki kontrolne z danych wejściowych i sprawdza ważność kodowania UTF-8, więc dane formularza zawsze będą czyste. W przypadku pól wyboru i arkuszy radiowych weryfikuje, czy wybrane pozycje rzeczywiście pochodziły z oferowanych i nie zostały spreparowane. Wspomnieliśmy już, że dla jednolinijkowych danych tekstowych, usuwa ona znaki końca linii, które mógłby wysłać atakujący. Dla wejść wieloliniowych normalizuje znaki dla przerw w linii. I tak dalej.

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

Wspomniany wcześniej atak CSRF polega na tym, że atakujący zwabia ofiarę na stronę, która w subtelny sposób wykonuje w przeglądarce ofiary żądanie do serwera, na którym ofiara jest zalogowana, a serwer zakłada, że żądanie zostało wykonane przez ofiarę 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 wysłanie formularza z innej domeny, użyj:

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

Ta ochrona wykorzystuje plik cookie SameSite o nazwie _nss. Ochrona plików cookie SameSite może nie być w 100% niezawodna, dlatego zaleca się włączenie ochrony tokenów:

$form->addProtection();

Zalecane jest zabezpieczenie w ten sposób formularzy w części administracyjnej witryny, które zmieniają wrażliwe dane w aplikacji. Framework broni się przed atakami CSRF, generując i weryfikując token autoryzacji, który jest przechowywany w sesji. W związku z tym konieczne jest, aby sesja była otwarta, zanim formularz będzie mógł być przeglądany. W części administracyjnej witryny sesja jest już zazwyczaj rozpoczęta z powodu zalogowania się użytkownika. W przeciwnym razie rozpocznij sesję za pomocą metody Nette\Http\Session::start().

Ten sam formularz w wielu prezenterach

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

Klasa fabryczna może wyglądać tak:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Jméno:');
		$form->addSubmit('send', 'Přihlásit se');
		return $form;
	}
}

Prosimy klasę o wyprodukowanie formularza w metodzie factory na komponentach w prezenterze:

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

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

Obsługa formularza może być również dostarczona z fabryki:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Jméno:');
		$form->addSubmit('send', 'Přihlásit se');
		$form->onSuccess[] = function (Form $form, $data): void {
			// zde provedeme zpracování formuláře
		};
		return $form;
	}
}

Mamy więc za sobą szybkie wprowadzenie do formularzy w Nette. Spróbuj spojrzeć w katalogu przykładów w dystrubucji, aby uzyskać więcej inspiracji.

wersja: 4.0