Niezalogowany [ logowanie ]
Subskrybuj kanał ATOM Kanał ATOM

Autor wpisu: Piotr Śliwa, dodany: 30.01.2010 13:28, tagi: php

Dziś pora na trzeci artykuł z serii "wzorce w praktyce". Tym razem postanowiłem omówić praktyczne zastosowanie mniej znanego i stosowanego (w programowaniu w php) wzorca.

Każdy z Was zapewne wie co to jest transakcja bazodanowa. Jeśli jednak nie, to przypomnę, że transakcja w systemach baz danych polega na tym, że określony zbiór zapytań wykona się poprawnie w całości lub wogóle żadne zapytanie nie zostanie wykonane (de facto "nie wykona się" jest błędnym określeniem, raczej "nie zostanie zatwierdzone"). Więcej na ten temat znajdziecie na wikipedii. Transakcja jest implementacją wzorca projektowego koordynator, a raczej rozbudowanej specjalizacji tego wzorca o nazwie "Zatwierdzanie trójfazowe" (Three-Phase Commit) - właśnie tą odmianę Koordynatora będe omawiał.

Wyjątkowo w skrócie opiszę ideę tego wzorca, gdyż jest on mniej powszechny niż dwa poprzednie wzorce opisywane przeze mnie. Uczestnikami są Koordynator, Użytkownik, Zadanie oraz Klient. Klient zleca Koordynatorowi do wykonania Zadanie, które jest podzielone na kilka części, jedna część Zadania może być wykonana tylko przez jednego Użytkownika. Rola Koordynatora polega na zapewnieniu spójności systemu, czyli podzielne Zadanie musi się wykonać w całości lub żadna z części tego Zadania przydzielona do jednego Użytkownika nie może się w ogóle wykonać.

Przedstawienie problemu

W naszym przykładzie Zadaniem będzie zaktualizowanie jakiegoś rekordu w kilku zdalnych i niezależnych usługach sieciowych będących na różnych serwerach, komunikacja odbywać się może przykładowo przez SOAP. Dla nas ważne jest, aby w każdej usłudze sieciowej był rekord o tym samym stanie, nie możemy dopuścić do sytuacji w której rekord w jednej z usług ma inny stan niż ten rekord w pozostałych usługach.

Rozwiązanie nie stosując Koordynatora

[PHP]
  1. //tablica obiektów reprezentujących różne usługi sieciowe
  2. //dla ułatwienia przyjmuję, że mają taki sam interfejs - jeśli
  3. //tak nie jest, można zastosować wzorzec Adapter opisywany w pierwszym
  4. //artykule z cyklu
  5. $services = ...;
  6.  
  7. //aktualny rekord, który ma być zapisany w zdalnych usługach
  8. $record = ...;
  9. foreach($services as $service)
  10. {
  11. //zapisanie rekordu w jednej ze zdalnych usług
  12. $service->save($record);
  13. }

Kod ten będzie działał, ale jeśli pewnego razu wystąpi problem z połączeniem z jedną ze zdalnych usług lub z innych nieistotnych powodów rekord nie zostanie zapisany, to stracimy spójność - ten sam rekord będzie miał inne wartości w różnych usługach.

Rozwiązanie problemu

Wydzielamy dwie klasy: Koordynatora (Coordinator) oraz Użytkownika (Coordination).

Najpierw zajmiemy się szkieletem klasy Użytkownika (Coordination). Obiekt, który może być koordynowany powinien umieć określić (nie wykonując swojej części zadania) czy wykonanie zadania się powiedzie (metoda prepare()), anulować wykonywanie zadania jeśli metoda prepare() zwróciła false - obiekt ten nie jest gotowy na jego wykonanie (metoda abort()). Powinien również umieć zatwierdzić (wykonać) zadanie (metoda commit()) oraz opcjonalnie wycofać zmiany, które nastąpiły po pomyślnym zatwierdzeniu (metoda rollback()).

[PHP]
  1. class Coordination
  2. {
  3. //obiekt jednej usługi zdalnej
  4. private $service = null;
  5. private $state = null;
  6. private $isAborted = false;
  7.  
  8. public function __construct($service)
  9. {
  10. $this->service = $service;
  11. }
  12.  
  13. /**
  14.   * @return bool Czy zadanie ma szansę się powieść?
  15.   */
  16. public function prepare()
  17. {
  18. try
  19. {
  20. //wykonanie jakiegoś testowego żądania, można np. pobrać w tym
  21. //żądaniu obecny obiekt rekordu który ma być zaktualizowany,
  22. //aby przy ewentualnym wywołaniu metody rollback() obiekt ten dysponował
  23. //stabilnym (przed wykonaniem zadania) stanem rekordu.
  24. $this->state = $this->service->test();
  25. }
  26. catch(Exception $e)
  27. {
  28. //załóżmy, że jeśli zostanie wyrzucony wyjątek, to żądanie testowe nie powiodło się
  29. //poprawność testu można stwierdzić również np. po wartości zwracanej
  30. return false;
  31. }
  32.  
  33. return true;
  34. }
  35.  
  36. /**
  37.   * Anulowanie wykonania zadania
  38.   */
  39. public function abort()
  40. {
  41. $this->isAborted = true;
  42. $this->state = null;
  43. }
  44.  
  45. /**
  46.   * Zatwierdzenie zadania
  47.   * @throws Exception Zadanie nie powiodło się
  48.   */
  49. public function commit()
  50. {
  51. if($this->isAborted)
  52. {
  53. throw new LogicException('Zadanie zostało anulowane');
  54. }
  55.  
  56. $this->service->doTask(...);
  57. }
  58.  
  59. /**
  60.   * Przywrócenie starego stabilnego stanu
  61.   */
  62. public function rollback()
  63. {
  64. $this->service->doTask($this->state);
  65. }
  66. }
W zamyśle metoda abort() wykona się jeśli metoda prepare() zwróci false, jeśli jednak zwróci true to powinna zostać wykonana metoda commit(). Jeśli metoda commit() wyrzuci jakiś wyjątek (zadanie nie powiodło się), to powinna zostać wykonana metoda rollback(). Pamiętajmy jednak, że jeśli metoda prepare() i-tego obiektu Użytkownika zwróci false, to mają zostać wywołane metody abort() obiektów od 0 do i-1, gdyż dany obiekt który uważa że nie jest w stanie wykonać zadania, nie musi anulować swojej gotowości do jego wykonania, gdyż sam powiedział że się nie przygotował ;) To samo tyczy się metody commit(), jeśli i-ty obiekt wyrzuci w niej wyjątek, to powinna zostać wywołana metoda rollback() obiektów od 0 do i-1, gdyż i-ty obiekt tego zadania nie wykonał.

Klasa Koordynatora powinna umieć zarejestrować Użytkowników (metoda register()) oraz w skoordynowany sposób wykonać zadanie (metoda commit()).

[PHP]
  1. class Coordinator
  2. {
  3. private $coordinations = array();
  4. private $index;
  5.  
  6. public function register(Coordination $coordination)
  7. {
  8. $this->cordinations[] = $coordination;
  9. }
  10.  
  11. public function commit()
  12. {
  13. $count = count($this->coordinations);
  14. $abort = false;
  15. //sprawdzenie, czy wszyscy uczestnicy są gotowi na wykonanie swojej części zadania
  16. for($this->index=0; $this->index < $count; $this->index++)
  17. {
  18. if(!$this->coordinations[$this->index]->prepare())
  19. {
  20. $abort = true;
  21. break;
  22. }
  23. }
  24.  
  25. //jeden z Użytkowników nie jest gotowy na wykonanie zadania, anuluj zadanie
  26. if($abort)
  27. {
  28. for($i=($this->index-1); $i >= 0; $i--)
  29. {
  30. $this->coordinations[$i]->abort();
  31. }
  32.  
  33. return false;
  34. }
  35.  
  36. try
  37. {
  38. //zatwierdź zadanie
  39. for($this->index=0; $this->index < $count; $this->index++)
  40. {
  41. $this->coordinations[$this->index]->commit();
  42. }
  43. }
  44. catch(Exception $e)
  45. {
  46. //jeden z Użytkowników nie zdołał zatwierdzić zadania, wycofaj zmiany
  47. for($i=($this->index-1); $i >= 0; $i--)
  48. {
  49. $this->coordinations[$i]->rollback();
  50. }
  51.  
  52. return false;
  53. }
  54.  
  55. return true;
  56. }
  57. }

Czytaj dalej tutaj (rozwija treść wpisu)
Czytaj dalej na blogu autora...

Autor wpisu: batman, dodany: 29.01.2010 10:42, tagi: zend_framework

Zend Framework oferuje wiele różnych sposobów na przechowywanie wersji językowych. Spośród wszystkich wymienionych, najbardziej zachwalany jest Zend_Translate_Adapter_Gettext. Adapter ten, w przeciwieństwie do większości pozostałych, wymaga narzędzi do stworzenia odpowiedniego pliku (o rozszerzeniu mo), który nie jest możliwy do odczytania przez człowieka. Dokumentacja poleca skorzystanie w tym

Autor wpisu: Zyx, dodany: 27.01.2010 17:45, tagi: php

Ostatnio w wolnych chwilach trochę zastanawiałem się, jak mógłby wyglądać następca PHP. Nie chodzi mi o jakieś mityczne PHP 7, ale o zwyczajne wzięcie się i zaprojektowanie tego języka od zera, na wstępie wyrzucając wszystkie irytujące niedociągnięcia. W tym wpisie chciałbym podzielić się wynikami tego eksperymentu myślowego.

Autor wpisu: Wojciech Sznapka, dodany: 25.01.2010 21:32, tagi: symfony, php, doctrine

Few minutes ago Brent Shaffer asked on the Twitter Which is more standard, „public static function” or „static public function”? I was curious about it, so I’ve checked which convention is used in my favourite Symfony Project. Of course, I haven’t got enough time to check it manually, class by class, so I wrote simple [...]

Autor wpisu: batman, dodany: 25.01.2010 21:06, tagi: internet

W dniu dzisiejszym na oficjalnym blogu Google pojawiła się informacja o wydaniu stabilnej wersji przeglądarki, oznaczonej numerem 4. Tym samym z fazy beta wyszły dwie najbardziej oczekiwane funkcjonalności – rozszerzenia i synchronizacja zakładek. Rozszerzenia (extensions) są “prostymi” skryptami napisanymi w języku Javascript, znacznie wzbogacającymi interfejs przeglądarki oraz dodają do niej

Autor wpisu: Piotr Śliwa, dodany: 24.01.2010 07:22, 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();

Czytaj dalej tutaj (rozwija treść wpisu)
Czytaj dalej na blogu autora...

Autor wpisu: batman, dodany: 23.01.2010 22:54, tagi: zend_framework

Od piątku mamy możliwość korzystać z odświeżonej dokumentacji Zend Framework. Oprócz zmian w wyglądzie (kolorowanie składni – nareszcie!), wprowadzono bardzo pomocną funkcjonalność – możliwość wyboru wersji frameworka. Dzięki temu wyszukiwanie informacji do wersji, która nas interesuje jest znacznie prostsze. Dokumentację można przeglądać od wersji 1.0, do najbardziej aktualnej 1.10, która jest
Wszystkie wpisy należą do ich twórców. PHP.pl nie ponosi odpowiedzialności za treść wpisów.