Niezalogowany [ logowanie ]
Subskrybuj kanał ATOM Kanał ATOM    Subskrybuj kanał ATOM dla tagu php Kanał ATOM (tag: php)

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: 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: 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: cojack, dodany: 22.01.2010 10:41, tagi: php

Tak więc w poprzednim wpisie, pokazałem w jaki sposób zaimplementować taką strukturę w sql, w tym wpisie chciałbym rozważyć pewne dodatkowe możliwości, które mogą chodź nie muszą okazać się przydatne w cale. A więc, mamy tutaj sprawdzanie uprawnień tylko i wyłącznie dla grup użytkowników. Co nie było naszym założeniem, więc by kontynuować i rozszerzyć możliwości naszego rbac’a należy wprowadzić pewne zmiany. Tabela rbac_privilages powinna zostać przemianowana na rbac_group_privilages, musimy też utworzyć drugą tabelę rbac_user_privilages o podobnej strukturze jak ta poprzednia. Taka zmiana pozwoli nam już dodawać indywidualne uprawnienia dla użytkowników. Z założeń pozostało nam jeszcze grupa do grup zadań czyli naszych controllerów, oraz użytkownik do grupy zadań. Jakby nie patrzeć są to dodatkowe dwie tabele, których utworzenie nie powinno przysporzyć większych problemów.

Logika struktury sql

Można by się zapytać po co tyle tabel? A bardzo chętnie na to pytanie odpowiem, otóż baza danych nie jest stworzona po to by trzymać w niej to co się chce i nawet bez sensu, takie rzeczy dyskryminują nas od razu w oczach pracodawcy. Więc dane nie powinny się powtarzać, kolumny powinny być tak tworzone by zawierały jak najmniej wartości NULL chyba że ma to pewien sens. Coś na zasadzie “Dziel i zwyciężaj” tylko tutaj prawda nie mamy żadnych algorytmów :)

Priorytety w Role Based Access Control

Mamy taki problem: Czy pobrać wszystko za jednym zamachem i sprawdzać dostęp za pomocą tego co mamy, czy sprawdzać pobierając dane po kolei? I odpowiedź bez sprawdzenia tego, które z założeń jest szybsze jest chyba absurdem, niestety nie mam na razie na to czasu by to sprawdzić, lecz chciałbym ustalić kolejność priorytetów. Założenie jest takie: Każdy użytkownik jest przypisany do jakiejś grupy, użytkownik bez grupy też jest w grupie użytkowników a mowa tutaj o osobach które nie są zarejestrowane na naszej stronie, można przyjąć ich za grupę Guest, czyli gości. Taki paradoks. I teraz priorytety wg mnie można by ustalić w ten sposób:

  1. Sprawdzanie czy grupa ma dostęp do controllera, czyli naszej grupy zadań.
    1. Ma, sprawdzamy czy grupa ma dostęp do akcji w kontrolerze, czyli naszego zadania
      1. Ma, zezwalamy
      2. Nie ma, przechodzimy do pkt 2
    2. Nie ma, przechodzimy do pkt 2
  2. Grupa nie ma uprawnień do zadania w controllerze, sprawdzamy czy użytkownik ma dostęp do controllera
    1. Ma, sprawdzamy czy ma dostęp do akcji w kontrolerze
      1. Ma, zezwalamy
      2. Nie ma, odmawiamy
    2. Nie ma, odmawiamy
  3. Amen

Hierarchia, czyli dziedziczenie uprawnień

Szczerze jak sobie pomyślę o takim założeniu to zaczyna mnie głowa boleć, bo już sobie w głowie tworzę taką aplikację w sql która musi to sprawdzać, no daję głowę, szczęka opada… Więc nie wiem czy aby na pewno jest to słuszne by tworzyć to, ale słowo się rzekło, to i się to napisze. Tylko obawiam się że bez rekurencji to nie przejdzie, chociaż nie jestem tego jeszcze do końca pewien, być może nie będzie tak źle na jak to wygląda. Dodatkowe akcje wchodzą w grę, należy sprawdzać czy jeżeli np grupa nie ma uprawnień do akcji, czy nad grupa ma pozwolenie na tą akcję oraz czy pozwala ją dziedziczyć, gdyby ktoś wyjechał z pomysłem komu pozwala ją dziedziczyć to w ogóle jeszcze więcej roboty, więc proszę nie dobijać leżącego :) Huh chociaż dobrze że nie ma sensu robić hierarchie użytkowników, bo od tego jest rbac właśnie by zrobił to za nas.

Słów kilka na zakończenie

Myślę że jest to optymalne sprawdzanie, chyba że coś pominąłem to mnie poprawcie, trochę z rana mogę być jeszcze nie ogarnięty z wszystkim, więc pisać w komentarzach co i jak.

Autor wpisu: thejw23, dodany: 19.01.2010 22:43, tagi: php

Inclued jest ciekawym rozszerzeniem php pozwalajacym zrzut do pliku informacji na temat includowanych plikow. nie jest to zazwyczaj do niczego niezbedne, ale czasami milo popatrzec na takie wykresy, aby moc ocenic stopien komplikacji frameworkow czy rozwiazan eccomerce (lub wlasnych CMSow itd).zrodla rozszerzenia sa dostepne np. na stronie domowej autora (wersja 0.3), a w sieci jest kilka przykladow jak to zrobic pod linuxem (np. tutaj), jednak nigdzie nie znalazlem informacji na temat uchomienia tego pod windowsem. juz mialem kompilowac, ale w koncu udalo wyszperac gotowa wersje, w postaci pliku dll. calosc testowana na xampp z php 5.3. tak wiec:1) pobieramy z tego adresu odpowiednia wersje pliku. szukamy plikow 'php_inclued-' z odpowiednia dla nas koncowka. phpinfo() prawde powie, u mnie to bylo php_inclued-5.3-svn20091211-VC6-x86.zip (ew. wersja z VC9, nie pamietam, na pewno wersja 'nts')2) dodajemy w php.ini nowy wpis 'extension=php_inclued.dll' oraz 'inclued.enabled = On' - nie dodajemy inclued.dumpdir, u mnie to nie chcialo dzialac, generowalo puste pliki.3) restart apache i mozna testowac4) w wybranym pliku bedacym punktem wejscia* do frameworka czy innego systemu (zazwyczaj index.php) dodajemy na samym koncu:$fp = fopen("dump.me", "w");fwrite($fp, serialize(inclued_get_data()));fclose($fp);co stworzy/nadpisze plik dump.me i doda do niego zrzut includowanych klas. nastepnie uruchamiamy przegladarke, wchodzimy na strone, a jak sie zaladuje, sprawdzamy czy jest plik dump.me,teraz potrzebujemy plik gengraph.php, do sciagniecia wraz ze zrodlami inclued czyli tutaj. uruchamiamy go z lini polecen, cmd i pozniej: php.exe /sciezka/do/pliku/gengraph.php -i /sciezka/do/pliku/dump.mew efekcie otrzymamy plik inclued.out.dot - polowa sukcesu za nami. teraz potrzebny jest Graphviz, ktory to pobieramy i instalujemy. dzieki niemu przerobimy .dot na ladny wykres. po zainstalowaniu Graphviz przechodzimy do katalogu gdzie jest zainstalowany, nastepnie do bin i uruchamiamy z linii polecen:dot -Tpng -o inclued.png /sciezka/do/pliku/inclued.out.doto i mozemy cieszyc sie wykresami. przyklady wygenerowane dla:KohanaPHP 2.3.4 - clickKohanaPHP 2.4RC2 - clickKohanaPHP 3.0.3  - clicknie sa to schematy idealne. wygenerowane sa na podstawie stron startowych frameworka, ktore nie korzystaja z bazy danych, a 3.0.3 nie korzysta z prawie niczego (co nie zmienia faktu, ze jest to bardzo czysty wykres i dobrze przemyslany framework).

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

Autor wpisu: Damian Tylczyński, dodany: 16.01.2010 18:08, tagi: internet, php

Przemyślenia dot. aktualnego modelu projektowania aplikacji internetowych i propozycja jego uporządkowania.
Wszystkie wpisy należą do ich twórców. PHP.pl nie ponosi odpowiedzialności za treść wpisów.