Do napisania poniższego tekstu zainspirował mnie znaleziony niedawno przeze mnie bardzo prosty ale jednocześnie przydatny plugin sfDoctrineGuardLoginHistoryPlugin autorstwa Christiana Schaefera. Plugin odpowiedzialny jest za odnotowywanie w bazie danych każdego zalogowania i wylogowania dowolnego użytkownika (trzeba najpierw zainstalować najpopularniejszą spośród wszystkich wtyczek: sfDoctrineGuardPlugin). W tym krótkim artykule chciałbym zaprezentować jak wygodnym i jak potężnym narzędziem są zdarzenia w symfony.
zdarzenia (symfony events)
W dużym skrócie, zdarzenia to mechanizm polegający na podpinaniu implementowanych funkcjonalności pod pewną „etykietę” (jakąś wybraną nazwę), np. „xyz„. W trakcie działania skryptu pada komenda „uwaga wszyscy! xyz!” – i zaraz po tym wykonywane są wszystkie czynności podwiązane pod tą etykietę. Dzieje się to automatycznie, przy czym trzeba określić w plikach konfiguracyjnych (pluginu lub całego projektu) które czynności są podpięte pod które zdarzenia.
Przykładowo, złożenie przez klienta zamówienia w koszyku sklepu internetowego, mogłoby oznaczać:
- zapisanie danych dotyczących zamówienia,
- wysłanie klientowi maila ze szczegółowymi informacjami dotyczącymi zamówienia, płatności itp.,
- dodanie tego klienta do odpowiedniej grupy na podstawie asortymentu jaki nabył (potencjalne kampanie marketingowe),
- zapisanie przypomnienia dla pracowników sklepu w celu namówienia klienta do ponownych zakupów – przypomnienie wyświetli się pracownikom np. miesiąc po złożeniu zamówienia itd.
Na tym przykładzie widać, jak wiele różnych czynności zostanie wymuszonych przez wykonanie innej czynności. Pojawia się pytanie – jak to zapisać?
- można oczywiście obudować to elegancko w metody OOP i wywoływać je za każdym razem kiedy mamy do czynienia ze „złożeniem rzez klienta zamówienia„,
- ale można też użyć w tym celu własnych zdarzeń: określamy jakie są zdarzenia, podpinamy pod nie odpowiednie metody odpowiednich klas – i w każdym miejscu w kodzie gdzie „klient składa zamówienie” rejestrujemy to zdarzenie
Najważniejszą zaletą drugiego rozwiązania wg mnie jest wygoda/elastyczność. Kiedy już mamy zdefiniowane zdarzenia i metody, wywołanie ich w innym miejscu to tylko jedna linijka – zarejestrowanie zdarzenia („uwaga wszyscy! xyz!„). Dalej: przejrzystość kodu, widziana z poziomu całego projektu (zwłaszcza kiedy wykonanie jednej czynności wymusza wykonywanie wielu innych – a to się często zdarza w dużych projekach). Pierwsze rozwiązanie, czyli bezpośredni ciąg wywołań metod „jedna po drugiej”, może zaciemnić obraz całości i z pewnością utrudni przyszłe modyfikacje. Wreszcie po trzecie, każde zdarzenie to swoista bramka pod którą można podpiąć cokolwiek innego w projekcie. To tak jakby zamiast zamurowywać ściany, wstawiać drzwi – aby się dostać do środka, nie trzeba kuć ścian – wystarczy otworzyć te drzwi.
zdarzenie w sfDoctrineGuardLoginHistoryPlugin
Symfony ma kilkadziesiąt wbudowanych zdarzeń (dokumentacja symfony 1.4). Jedno z nich – user.change_authentication – zostało wykorzystane właśnie w sfDoctrineGuardLoginHistoryPlugin. W pliku konfiguracyjnym config/sfDoctrineGuardLoginHistoryPluginConfiguration następuje podwiązanie zdarzenia z określoną metodą:
public function initialize() { $this->dispatcher->connect('user.change_authentication', array('UserLoginHistoryTable', 'writeLoginHistory')); }
Zgodnie z dokumentacją symfony, zdarzenie to jest rejestrowane (tzn. „dzieje się”) zawsze kiedy zmienia się stan uwiarygodnienia (login/logout) użytkownika (event is notified whenever the user authentication status changes). Pod to zdarzenie podpięta jest tylko jedna metoda, UserLoginHistoryTable::writeLoginHistory(), która polega na zapisaniu w bazie danych informacji o odpowiednio: zalogowaniu bądź wylogowaniu danego użytkownika.
static public function writeLoginHistory(sfEvent $event) { $sessionUser = $event->getSubject(); $params = $event->getParameters(); if(true === $params['authenticated']) { $userId = $sessionUser->getGuardUser()->id; $sessionUser->setAttribute('user_id', $userId, 'sfDoctrineGuardLoginHistoryPlugin'); self::createHistoryEntry('login', $userId); } else { $userId = $sessionUser->getAttributeHolder()->remove('user_id', null, 'sfDoctrineGuardLoginHistoryPlugin'); self::createHistoryEntry('logout', $userId); } } protected static function createHistoryEntry($state, $userId) { $history = new UserLoginHistory(); $history->state = $state; $history->user_id = $userId; $history->ip = getenv('HTTP_X_FORWARDED_FOR') ? getenv('HTTP_X_FORWARDED_FOR') : getenv('REMOTE_ADDR'); $history->save(); }
W efekcie, nie musimy wchodzić wgłąb kodu sfDoctrineGuardPlugin, rozdłubywać mechanizm logowania i modyfikować go, wstawiając wywołanie w/w metody. Tak naprawdę to w ogóle nie musimy wiedzieć jak skonstruowany jest ten mechanizm. Wiemy tylko że (wy)logowanie rejestruje zdarzenie user.change_authentication i się pod nie podpinamy – nic więcej nas nie interesuje a jednocześnie wszystko działa tak jak powinno. Proste, szybkie, skuteczne i efektywne.
Predefiniowane w symfony zdarzenia stanowią zatem swoisty interfejs pod który można się niesamowicie łatwo podpiąć, jak widać na przykładzie omawianego pluginu (symfony pozostawia nam wiele drzwi które możemy otwierać bez konieczności burzenia ścian). W większych projektach oczywiście warto tworzyć własne zdarzenia i podpinać pod nie swoje funkcjonalności. Wówczas włączenie/wyłączenie danego mechanizmu to tylko za(od)komentowanie jednej linijki, w której rejestruje się daną metodę dla danego zdarzenia.
PS to mój pierwszy artykuł po polsku – mam nadzieję że piszę w sposób w miarę zrozumiały
Dobry wpis. Bardzo przyjemnie się czyta
Z doświadczenia wiem, że ze zdarzeniami trzeba ostrożnie. czasem potrafią odnieść odwrotny skutek i zaciemnic kod projektu (w końcu uruchamiają kod, który bywa poza normalnym „flowem” aplikacji).
Dzięki
Oczywiście, nie można przesadzać i naszpikować projektu zdarzeniami – wtedy bardzo ciężko byłoby wytropić błąd czy też – jak napisałeś – kontrolować przetwarzanie requestów. Tak jak ze wszystkim – trzeba z umiarem
Ja stosuję zdarzenia w swoich projektach – o ile są na tyle duże żeby zdarzenia miały w ogóle sens – tylko dla centralnych, bardzo rozbudowanych mechanizmów. Kiedy jedna czynność pociąga za sobą co najmniej 3-4 inne czynności – wtedy uznaję wykorzystanie zdarzeń za uzasadnione.
Fajnie, że ktoś umieścił w końcu tag „more” – strasznie nie lubię przewijać strony w poszukiwaniu nowych wpisów. Poza tym artykuł bardzo interesujący – myślę, że pomimo tego, że niedługo pojawi się Symfony2, dalej będzie spore zapotrzebowanie na materiały związane z jedynką. ;]
Jestem pewien, że zapotrzebowanie na „jedynkę” będzie bardzo duże nawet wiele miesięcy po ostatecznym wydaniu symfony2. Symfony 1.4 jest naprawdę świetnym narzędziem i sporo firm rozwija i będzie rozwijać oprogramowanie na tej platformie – a przejście na wersję 2 będzie oznaczało ogrom pracy i ogromne koszty – trzeba większość zbudować od zera (sam się zastanawiam czy w przypadku mojej firmy upgrade na sf2 w ogóle będzie możliwy).
Znajdowałem na forach nawet programistów których firmy „wpakowały się” w tworzenie oprogramowania w symfony 1.0 i nie zamierzają robić upgrade’u. No i biedni poszukiwali odpowiedzi dla takiego starocia (to było jakoś rok temu)
Przejście z 1.0 na 1.4 może jeszcze da się jakoś przeboleć, ale 1.4 -> 2.0 to praktycznie niemożliwe. Ja sam się zastanawiam, w który framework „włożyć ręce”, bo czytam sporo o 2.0 i strasznie mi się podoba, ale z drugiej strony znam się całkiem nieźle na 1.4, co oznacza mniej nauki i więcej pracy, jaką mogę wykonać. ;]
Ja mam tak samo – wiedzę nt. 2.0 mam nieporównywalnie mniejszą niż 1.4 (i właśnie w 1.4 mam napisany ogromny projekt, który będzie rozwijany pewnie jeszcze długo). Ale z drugiej strony boję się, że po 2 latach 1.4 zostanie całkowicie opuszczone, nowi deweloperzy symfony nie będą się już interesować starociami. Jeśli masz pisać coś na wiele lat, na Twoim miejscu bym uderzał w 2.0 raczej. A jeśli po prostu nie wiesz w co się wgłębiać, to chyba też bym wybrał sf2