Obsługa zdarzeń w PHP
Ostatnio przypadło mi zmierzyć się z pewnym problemem, który wymusił na mnie utworzenie mechanizmu obsługi zdarzeń w rozwijanym przeze mnie frameworku. Postaram się pokrótce opisać problem i rozwiązanie, jakiego użyłem.
Jako, że mój framework oparty jest o MVC, posiadam klasę modelu zawierającą wszystkie metody i właściwości potrzebne do komunikacji z bazą danych. Klasa zwie się PBlock (PrimeBlock). Z tej klasy dziedziczą wszystkie inne, a między innymi klasa ParentBlock będąca rodzicem wszystkich modeli artykułów (w CMSie Artykułów, Newsów, Galerii itd.). Dodatkowo z PBlock mogą dziedziczyć inne klasy takie jak Language czy Section. Daje to drzewiastą strukturę różnego typu modeli.
PBlock posiada kilka strategicznych metod, które aż się proszą o objęcie kontrolą. Tymi metodami są: read, write, edit i remove. Odpowiadają one za zapytania kolejno: SELECT, INSERT, UPDATE i DELETE. Wcześniej radziłem sobie dodając przed strategicznymi metodami znak "_", a następnie tworząc metody mostkujące do strategicznych metod. Takie metody można było w prosty sposób przesłonić w klasie potomnej, i obsłużyć zdarzenie. Dla przykładu
function read() Było to bardzo prymitywne obsłużenie zdarzeń, ale wykonanie go nie było żadnym wyzwaniem i pozwlało na błyskawiczną implementację. Niestety ten prymitywny sposób legł w gruzach w momencie, gdy postanowiłem zaimplementować dla klasy ParentBlock mechanizm sprawdzający czy użytkownik otwierający dany typ artykułu z poziomu panelu ma dostęp do sekcji, w której ów artykuł się znajduje. Wcześniej owe rozwiązanie zaimplementowane było po stronie widoku, ale trzeba było je przepisywać i za każdym razem pilnować, by nie zrobić błędu.
{
// kod przed
$this->_read();
// kod po
}
W tym wypadku niestety nie mogłem przesłonić metody read, bo uniemożliwiłbym jej prawidłowe funkcjonowanie w przypadku próby obsłużenia zdarzenia read przez klasę potomną. W ten oto sposób narodził się pomysł utworzenia modułu słuchającego odpowiedzialnego za obsługę zdarzeń.
Nie jest to w zasadzie żadne rewolucyjne rozwiązanie, ale osobiście nigdy nie spotkałem zadowalającego mnie modułu rozwiazującego problem zdarzeń w PHP.
Na początek wypadało by utworzyć jakieś założenia. I tak, do obsługi zdarzeń potrzebujemy klasy zdarzenia (najczęściej odpowiadającej klasie wywołującej zdarzenie), nazwy zdarzenia i jego typu. W moim przypadku są to typy "pre" i "post" dla przed i po - zdarzenia. Jako opcjonalny element dodałem filtr klasy zlecającej zdarzenie by potomek klasy wysyłającej zdarzenie przyjmował tylko te, które odnosi się do niego.
Wszystkie powyższe elementy można w prosty sposób umieścić w tablicy łącząc je w całość poprzez utworzenie osobnego indeksu. I tak dla obiektu PBlock dla przed - zdarzenia "read" mamy indeks "pblock:read:pre". Indeks ów jest zwykłą tablicą przechowującą rekordy zdarzeń. Rekord zdarzeń to również tablica składająca się z nazwy funkcji przejmującej zdarzenie oraz z opcjonalnego parametru zawierającego typ klasy obiektu wywołującego zdarzenie.
Podczas wywoływania zdarzenia wystarczy by obiekt zlecający jego wykonanie przesłał klasę zdarzenia, nazwę zdarzenia, jego typ i ewentualne argumenty. Z owych danych rekonstruujemy indeks dla tablicy zdarzeń i pobieramy wszytkie potrzebne rekordy.
Taki prosty mechanizm zdarzeń można by pozostawić bez komentarza i w prosty sposób zaimplementować, ale w praktyce pojawia się pewna uciążliwość. Jest nią potrzeba rejestracji zdarzenia dla każdego nowoutworzonego obiektu, co w praktyce wymagałoby od nas przepisania dziesiątek linii kodu. Jest to bardzo uciążliwe rozwiązanie, dlatego postanowiłem odrobinę rozwinąć swoje rozwiązanie i wprowadzić możliwość rejestrowania zdarzeń we właściwościach klasy.
Każda klasa, która zamierza obsłużyć zdarzenie wpierw powinna posiadać zaimplementowany interfejs znaczący ją jako tę, która obsługuje zdarzenia. A następnie powinna zostać przypisana jej tablica metod obsługujących zdarzenia. Przy takim rozwiązaniu jedynym problemem pozostaje wywołanie mechanizmu, który zarejestruje dla nas zdarzenia, ale tę operację bez żadnych problemów może wykonać za nas konstruktor naszej klasy albo jak to ma miejsce w moim przypadku, gdy zdarzenie przesyłane jest od klasy matki do klasy potomnej, agregację metod słuchających można wykonać zaraz po wywołaniu pierwszego zdarzenia, ale przed operacją wykonywania funkcji je obsługujących.
W tym wypadku bardzo pomocna jest funkcja debug_backtrace zwracająca nam stos wywołań funkcji i metod zakończonych tą, w której się aktualnie znajdujemy. Z owego stosu w prosty sposób można ściągnąć referencję do obiektu wywołującego zdarzenie, przez co jesteśmy w stanie pobrać tablicę funkcji słuchających oraz podczas wywoływania zdarzenia jesteśmy w stanie sprawdzić czy jest instancją porządanej klasy, a następnie ową referencję przesłać jako parametr do funkcji obsługującej zdarzenie.
Powyższy przykład można by nazwać prymitywnym sposobem obsługi zdarzeń z poziomu PHP, ale uważam, że warto wspomnieć o kolejnym sposobie samego wywoływania zdarzeń. Jeśli posiadamy aplikację napisaną w taki sposób jak ten zaprezentowany przeze mnie, czyli że zdarzeniem jest samo wywołanie określonej funkcji, to po przemianowaniu funkcji, dla przykłądu z "read" na "_read" wcale nie musimy tworzyć jej odpowiednika. Wystarczy, że użyjemy konstrukcji __call($funkcja, $argumenty) i będziemy hookowali wszystkie odniesienia do naszych metod-zdarzeń.
Przykładowy kod może wyglądać tak:
Class klasa Jak widać jest to ciekawy i wygodny sposób wywoływania zdarzeń, ale nie pozwala on nam na pełną ingerencję w to kiedy ma się zaczynać, a kiedy kończyć specyficzne zdarzenie. Mimo to taki sposób zawsze może być pewną alternatywą, jedną z wielu opcji.
{
...
function __call($func, $args)
{
raiseEvent($func, "pre");
call_user_func(array($this, $func), $args);
raiseEvent($func, "post");
}
...
}
Proszę zważać na to co piszecie w komentarzach. Wpisy zawierające idiotyczne, obraźliwe, wulgarne, ubogie merytorycznie lub niezgodne z polskim prawem treści będą usuwane.
Michał
Napisz proszę coś więcej o rozwijanym przez Ciebie frameworku