Formularze są nieodzownym elementem stron WWW. Niestety ich definiowanie, walidacja i obsługa to zadanie bardzo żmudne (rzeźbienie struktury w HTML’u) i często dość trudne (walidacja danych, zabezpieczenie przed CSRF), a dodatkowo wymaga sporego nakładu czasu. Na szczęście coraz częściej wykorzystuje się gotowe biblioteki, które znacznie przyspieszają pracę.

UWAGA! Już w najbliższy wtorek 22.04 premiera kursu Symfony – Techniki Pracy na eduweb.pl! Więcej informacji znajdziecie w tym poście.

Symfony2 posiada genialne komponenty zarówno do pracy z formularzami jak i do walidacji danych. W tej lekcji chciałbym pokazać Wam, na przykładzie prostego formularza subskrypcji, jak wykorzystać możliwości frameworka i dosłownie w przeciągu kilku minut zdefiniować formularz oraz dodać reguły walidacji.

Pierwszą rzeczą, którą powinniśmy zrobić jest zdefiniowanie encji. Encje to zwykłe klasy PHP odzwierciedlające jakiś byt w naszym programie i może to być klasa użytkownika, klasa produktu, etc. Dobrą praktyką jest tworzenie klas encji w katalogu Entity w głównym katalogu pakietu. W naszym przypadku, klasa encji jest banalnie prosta:

W klasie mamy zdefiniowane dwie właściwości: name – nazwa użytkownika oraz email – email tej osoby. Ze względu na to, że są to właściwości prywatne musimy dodać metody dostępowe (gettery i settery). Ich brak może spowodować błędy w działaniu formularza. Dodatkowo warto zwrócić uwagę na linię 2, gdzie określamy przestrzeń nazw, w której znajduje się klasa, aby autoloader mógł ją znaleźć.

Kolejnym krokiem jest stworzenie klasy formularza, w której zdefiniujemy z jakich elementów będzie składał się formularz. Dobrą praktyką jest tworzenie tego typu klas w katalogu Form/Type. Warto też dodać do nazwy klasy przyrostek Type. Kod klasy formularza subskrypcji:

W tej klasie jest kilka miejsc, na które warto zwrócić uwagę:

linia 3: Określamy tu przestrzeń nazw klasy (odzwierciedla ona także strukturę katalogu,w którym znajduje się klasa).

linie 5,6,7: Importujemy wymagane klasy, z których będziemy korzystać.

linia 9: Podążamy za dobrymi praktykami i w nazwie klasy korzystamy z przyrostka Type. Dodatkowo dziedziczymy po klasie AbstractType (wymagane).

linia 11: Metoda getName() jest wymagana i musi ona zwracać nazwę tego formularza. Nazwę możemy określić samodzielnie.

linia 15: Przeciążamy metodę buildForm i to w niej, korzystając z obiektu buildera, definiujemy poszczególne pola formularza. Każde użycie metody add() dodaje nowy element do formularza. W metodzie add() najważniejsze są dwa pierwsze parametry. Pierwszy parametr powinien mieć taką samą nazwę jak właściwość w encji, drugi parametr określa typ pola. Lista wszystkich dostępnych typów wraz z ich opisem dostępna jest w referencji. Trzeci parametr (tablica) jest opcjonalny i to w nim możemy przekazać dodatkowe opcje dla danego pola. My ustawiamy jedynie label. Ostatnie wywołanie metody add() dodaje do formularza przycisk submit.

linia 28: Przeciążamy metodę setDefaultOptions i w niej ustawiamy najważniejszy domyślny parametr czyli data_class. Powinien on określać klasę, dla której ten formularz został stworzony. W naszym przypadku jest to stworzona wcześniej encja Subscription.

Teraz możemy wykorzystać gotową klasę SubscriptionType w kontrolerze do stworzenia instancji formularza. W katalogu Controller, pakietu utworzyłem, klasę dla kontrolerów SubscriptionsController.php, a w niej zdefiniowałem kontroler indexAction. Bazowy kod tej klasy wygląda następująco:

W celu lepszego rozumienia, na pewno przyda się krótkie omówienie, a zatem:

linia 3: Standardowo, definiujemy przestrzeń nazw.

linie 5-9: Importujemy klasy, z których będziemy korzystać.

linia 11: Klasa SubscriptionsController dziedziczy po bazowym kontrolerze w Symfony2. Dzięki temu mam dostęp do szeregu dziedziczonych metod (m.in. do createForm()).

linie 13-17: Używamy adnotacji @Route do zdefiniowania adresu URL pod którym możemy uruchomić kontroler oraz adnotacji @Template do automatycznego podpięcia pliku szablonu.

linia 20: Do zmiennej $Subscription przypisujemy nową instancję encji Subscription. Do tej instancji w dalszej części zbidnujemy dane z formularza.

linia 22: Linia kluczowa, w której za pomocą dziedziczonej metody createForm tworzymy instancję formularza na podstawie definicji SubscriptionType (pierwszy parametr). Opcjonalnie przekazujemy w drugim parametrze encję. Jeżeli zawierałaby ona ustawione właściwości, zostałby by one wstawione do pól formularza.

linia 25: Do widoku przekazujemy, to co zwraca metoda $form->createView() czyli instancję widoku formularza (FormView). Będzie ona dostępna w widoku pod zmienną ‚form’.

Wszystko jest już gotowe do tego, aby wyrenderować formularz na stronie. Tworzymy zatem plik szablonu TWIG dla tego kontrolera w katalogu: Resources/views/Subscriptions/index.html.twig. Kod wymagany do wygenerowania formularza jest banalnie prosty i sprowadza się do jednej linii:

Prostotę tego rozwiązania zawdzięczamy specjalnej funkcji form() w TWIGu, do której jako parametr przekazaliśmy formularz. Ze względu na to, że każdy element formularza, renderowany jest z atrybutem required, przeglądarki wspierające HTML5 uruchomią walidację po stronie klienta. Jeżeli chciałbyś to wyłączyć wystarczy że ustawisz atrybut novalidate w tagu <form>. Używając funkcji form() możemy to osiągnąć w ten sposób:

Wynik tego działania prezentuje się następująco:

sf2-rendered-form

Formularz wygląda elegancko, a dodatkowo Symfony 2 dodało nam jeszcze jedno pole, którego my nie widzimy, a mianowicie token zabezpieczający przed atakami CSRF! Super!

sf2-form-csrf-token

Pora na dodanie do kontrolera kodu obsługującego formularz po kliknięciu w przycisk „Zapisz się”. Najpierw prezentacja zaktualizowanego kodu:

a teraz omówienie różnic w porównaniu z kodem bazowym:

linia 10: zaimportowanie klasy Request

linia 19: wstrzyknięcie w nagłówku obiektu Request do kontrolera.

linia 25: za pomocą metody $form->handleRequest($Request) bindujemy do formularza dane wpisane przez użytkownika (znajdujące się w obiekcie $Request).

linia 26: w instrukcji warunkowej uruchamiamy metodę $form->isValid(), która zwraca true/false w zależności, czy wszystkie pola formularza są wypełnione poprawnie. W tym momencie nie mamy żadnych warunków nałożonych na pola name i email, więc zostanie sprawdzony tylko token CSRF przypisany do formularza.

linia 28: jeżeli $form->isValid() zwróci true, to znaczy, ze wszystkie pola formularza są poprawne i możemy przejść do zapisania obiektu $Subscription. Dane z formularza zostały do niego zbindowane w linii 25.

Ostatnim zadaniem jest dodanie walidacji do pól name oraz email. Załóżmy, że oba pola nie mogą być puste, a dodatkowo chcemy mieć pewność że użytkownik poda poprawny adres e-mail. Aby dodać takie reguły użyjemy adnotacji w klasie encji:

Myślę, że kod jest jasny, ale dla pewności go omówię:

linia 4: Importujemy przestrzeń nazw w której znajdują się wszystkie reguły walidacji dostępne w symfony. Ich listę możesz znaleźć w referencji. Dla tej przestrzeni nazw nadaliśmy alias Assert, z którego korzystamy później.

linia 9: Ustawiamy regułę walidacji NotBlank, która znajduje się w przestrzeni nazw Assert. Tyle wystarczy, aby sprawdzić czy pole nie jest puste.

linia 14,15: Ustawiamy regułę walidacji NotBlank (czy nie jest puste) oraz Email (czy poprawny adres e-mail) dla pola email.

Teraz możemy przejść do formularza i spróbować popełnić kilka błędów:

sf2-form-errors01 sf2-form-errors02

Jak widzicie, kod który musieliśmy napisać jest krótszy niż komentarz do niego. Dodatkowo, wydaje mi się, że kod jest na tyle czytelny, że nawet bez komentarza większość z Was nie miałaby problemu ze zrozumieniem poszczególnych etapów.

  • Witam,

    mam pytanie odnośnie formularzy. W jaki sposób zrobić, aby po przesłaniu formularza wszystkie jego pola zostały wyczyszczone. W tej chwili mam stworzony formularz, który zapisuje mi dane do bazy, ale po zapisie pola pozostają uzupełnione danymi, które były wcześniej zapisane.

    Pozdrawiam

    • Maciej Zukiewicz

      Panie Michale,

      najlepszą praktyką, jest przekierowanie użytkownika (Redirect) na tą samą stronę + dodanie komunikatu o poprawnie wykonanej akcji. Dzięki temu dane z formularza zostaną wyczyszczone, a dodatkowo zapobiemy sytuacji, w której po wciśnięciu F5 (odświeżenia) dane formularza znów zostaną przesłane POST’em.

      Pozdrawiam,
      Maciej Żukiewicz

  • Witam,
    ja natomiast chciałbym dopytać o taką sytuację:
    przypuśćmy że użytkownik może dodać albo edytować post. (addPost oraz editPost). Podczas dodawania/edycji wysyła formularz do jakiejś akcji – np. updatePost. W tej akcji formularz podlega walidacji ale zawiera błędy. Gdybym odnosił się do tej samej akcji w ktorej renderuję formularz (np. addPost oraz editPost), mógłbym „przechwycić” błędy formularza (form_errors) zwracając je w tej samej akcji. Jednak wtedy użytkownik ma nadal możliwosć wciśnięcia F5 i ciągłego odświeżenia formularza. Jak w takim razie zrobić tak, aby po wysłaniu formularza przekierować użytkownika na ten sam formularz (czyli redirect do addPost albo editPost) ale przechwycić błędy które powstały podczas walidacji (przekazać je jakoś do sesji?)?