Home / Brudnopis / Wielostronicowe formularze

Wielostronicowe formularze 23.01.2010

tagi: php symfony

Nie raz spotkałem się z problemem implementacji wielostronicowych formularzy w projektach w których uczestniczyłem, zazwyczaj były to formularze rejestracji, które składały się z 2-4 kroków. Podstawowe problemy które należy rozwiązać przy wykonywaniu formularza tego typu:

  • możliwie jak najprostszy, spójny i elastyczny sposób przetwarzania formularza, aby ewentualne dodanie nowego pola lub całego formularza kosztowało jak najmniej nakładu pracy

  • napisanie kodu, który będzie można również wykorzystać w przyszłości w innym projekcie


Istnieją dwa główne mechanizmy przechowywania danych z formularzy z poprzednich stron / kroków. Na pierwszy rzut oka zaprzęgnięcie sesji w tym celu wydaje się dobrym rozwiązaniem, jednakże czy tak w rzeczywistości jest? Sesja może wygasnąć podczas wypełniania długiego formularza lub też formularz może nie zostać w pełni wypełniony, co skutkuje przechowywaniem śmieci w sesji (rzutuje to również na wydajność). Oczywiście, można napisać coś w rodzaju garbage collection aby rozwiązać ten drugi problem, ale jest też inne rozwiązanie - przekazywanie danych z poprzednich kroków w ukrytych polach formularza. To również nie jest doskonałe, ale w mojej ocenie przysparza mniej problemów niż sesja.

Jeśli wiemy w jaki sposób przekazywać dane między żądaniami, zobaczmy jak to by wyglądało w praktyce.

(pseudokod)
[PHP]
  1. $forms = array(/* tablica obiektów formularzy dla poszczególnych stron*/);
  2.  
  3. if(/*wysłano formularz*/)
  4. {
  5. $page = /*strona obecnego formularza, załóżmy że numerowanie zaczyna się od 0 */;
  6. $values = /*dane wysłane postem*/;
  7. $fail = null;//pierwszy formularz, który nie przeszedł walidacji
  8.  
  9. //waliduj wszystkie formularze do ostatnio wysłanego
  10. for($i=0; $i<$page; $i++)
  11. {
  12. if(!$forms[$i]->isValid($values))
  13. {
  14. $fail = $forms[$i];
  15. break;
  16. }
  17. }
  18.  
  19. //wystąpił błąd
  20. if($fail)
  21. {
  22. //wyświetl $i-ty formularz z komunikatami błędów
  23. //oraz poprawnie zweryfikowane formularze jako ukryte pola
  24. }
  25. else
  26. {
  27. if(/* wysłano i zweryfikowano formularze ze wszystkich stron */)
  28. {
  29. //zapisanie danych do bazy danych i przekierowanie
  30. }
  31. }
  32. }
  33.  
  34. if(!$fail)
  35. {
  36. //wyświetl obecny formularz
  37. echo $forms[$page];
  38. for($i=0; $i<$page; $i++)
  39. {
  40. //wyświetl poprzednie formularz jako ukryte pola formularzy
  41. }
  42. }


W powyższym rozwiązaniu cała logika obsługi wielostronicowego formularza jest skumulowana w jednym miejscu. To znacznie lepsze rozwiązanie niż obróbka formularzy z kolejnych kroków w innej akcji kontrolera. Dzięki powyższemu rozwiązaniu dodawanie kolejnych formularzy nie stanowi problemu, należy jedynie do tablicy $forms dodać kolejny obiekt formularza (lub tablicę reprezentującą formularz?) na odpowiednim miejscu oraz ewentualnie zmienić kod wykonywany po weryfikacji wszystkich formularzy.

Pierwszy punkt z listy przedstawionej na początku artykułu został spełniony: sposób przetwarzania jest dosyć prosty, spójny i elastyczny. Pozostaje problem przenośności kodu.


Plugin do obsługi wielostronicowych formularzy dla symfony


Swego czasu napisałem plugin do obsługi wielostronicowych formularzy dla frameworku symfony. Jego główne zadania to rozdzielanie wartości parametrów, walidacja poszczególnych formularzy oraz ukrycie przed programistą problemu przenoszenia danych między żądaniami.

Tworzenie własnego wielostronicowego formularza.
[PHP]
  1. class Form extends psPageableForm
  2. {
  3. public function setup()
  4. {
  5. $this->addForm(new Form1());
  6. $this->addForm(new Form2());
  7. $this->addForm(new Form3());
  8.  
  9. $this->setNameFormat('form[%s]');
  10. }
  11. }


Jednym ze sposobów stworzenia wielostronicowego formularza jest nadpisanie klasy psPageableForm i w metodzie setup (lub configure) dodanie kolejnych formularzy.

Przetwarzanie formularza tego typu będzie zbliżone do tego zaprezentowanego na pierwszym listingu - algorytm jest niemalże ten sam, z tą różnicą, że część zadań wykonują klasy pluginu.

[PHP]
  1. //kontroler
  2. public function executeProcessForm(sfWebRequest $request)
  3. {
  4. $page = (int) $request->getParameter('step', 1);
  5. $form = $request->hasAttribute('form') ? $request->getAttribute('form') : new Form();
  6. $form->setPage($page);
  7.  
  8. if($request->isMethod('post'))
  9. {
  10. //jeśli ta metoda została wywołana po błędzie walidacji poprzedniego formularza
  11. //nie przeprowadzaj walidacji
  12. if($request->getAttribute('return') != 1){
  13. $form->bind($request->getParameter('form'));
  14. if(!$form->isValid()){
  15. //numer strony to numer pierwszego niepoprawnego formularza
  16. $page = $form->getFirstInvalidForm()->getOption('page');
  17. $request->setParameter('step', $page);
  18.  
  19. //utawić atrybuty żądania, aby przekazać zwalidowany formularz
  20. $request->setAttribute('return', 1);
  21. $request->setAttribute('form', $form);
  22.  
  23. //wywołanie rekurencyjne akcji aby wyświetlić formularz
  24. //który nie przeszedł walidacji
  25. return $this->executeProcessForm($request);
  26.  
  27. //zweryfikowano ostatni formularz
  28. }elseif($page > $form->getPages()){
  29. //zapisanie danych do bazy
  30. $this->redirect(/*przekierowanie*/);
  31. }
  32. }
  33. }
  34. else
  35. {
  36. $form->setPage(1);
  37. }
  38.  
  39. $this->form = $form;
  40. }
  41.  
  42. //widok
  43. echo $form->getCurrentForm();
  44. //wyświetlenie ukrytych pól formularza
  45. echo $form->persist();


Plugin ma zaimplementowane dwie strategie przekazywania danych między żądaniami: poprzez ukryte pola formularzy oraz sesja.

Można pójść jeszcze dalej z realizacją ponownego wykorzystania kodu i napisać odpowiednią klasę (np. ProcessPageableForm), która będzie w ogólny sposób hermetyzować przetwarzanie wielostronnicowego formularza (czyli wyżej przedstawiony kod kontrolera). W tym miejscu jednak zostawiam wolność w ewentualnej implementacji tego rozwiązania ;)

Kod źródłowy pluginu: klik

Komentarze (3)

#1 Sajam, 25.01.2010 21:22

Ja użyłem nieco innego sposobu. Czy bardziej elastyczny? Nie wiem...

Tworzę wpis w bazie danych z oznaczeniem krok=1, ostatnią datą modyfikacji, kluczem i kolejno uzupełniam pola. Przy pokazaniu formularza usuwam stare wpisy (niedokończone wypełnienie etc.). W cookie i bazie danych zapisuję sobie jakiś wygenerowany klucz. Przy wejściu w któryś formularz np. register/step_3 wyciągam z bazy krok przy którym jestem na podstawie klucza z cookie. Jeżeli zostanie wypełniony poprawnie znowu aktualizuję baze danych i zwiększam "krok". Jeżeli zakończyliśmy proces czyszczę pole "krok" i na tej podstawie identyfikuje późniejsze prawidłowe rekordy.

#2 piotrooo89 (www) , 25.01.2010 22:21

Tylko czy zaprzęganie db do tego jest dobrym rozwiązaniem, co jak potrzeba zrobić coś "lekkiego"? Rozwiązanie które podałeś jest fajne bo można cuda robić jeśli użytkownikowi zamknie się przeglądarka etc no ale jak wspomniałem wcześniej nie jest to 'lekkie' rozwiązanie.

#3 Sajam, 26.01.2010 10:25

Że to rozwiązanie jest "Ciężkie"? Dane i tak musisz zapisać w jakimś źródle danych, a że robię to bezpośrednio to chyba nie jest ciężkie rozwiązanie. No i łatwiej kodem zarządzać, nie trzeba się martwić o pre-definiowane pola, errory i ukryte przesyłanie danych. Tu wystarczy tylko klucz w cookie.

Dodaj komentarz