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"></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;
}
Alternatywnie można 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ż wyliczeniami i zostaną automatycznie zmapowane.
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) i 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.