PHP wraz z wersją oznaczoną numerem 5 oddało nam do dyspozycji zestaw bardzo ciekawy udogodnień, ułatwiających rozwiązywanie nietypowych problemów. Jedno z nich, które w dalszej części opiszę jest związane z inżynierią wsteczną (ang. reverse engineering).
Mowa tutaj o Introspekcji (ang. Reflection), a właściwie to interfejsie funkcji API pozwalający nam na pobieranie kompletnych informacji odnośnie:
- klas (ang. classes)
- interfejsów (ang. interfaces)
- metod (ang. methods)
To nie wszystko, automatycznie otrzymujemy także dostęp do dokumentacji tychże klas/funkcji/interfejsów (ang. doc comments). Poniżej podaję wybrane metody obiektów (nie wszystkie), które posłużą w dalszej pracy.
- class ReflectionClass implements Reflector
- {
- public string getDocComment()
- public ReflectionMethod getMethod(string name)
- public ReflectionMethod[] getMethods()
- public ReflectionClass getParentClass()
- public bool isSubclassOf(ReflectionClass class)
- public bool implementsInterface(string name)
- }
- class ReflectionMethod extends ReflectionFunction
- {
- public bool isPublic()
- public bool isPrivate()
- public bool isProtected()
- public string getName()
- }
- ?>
Przepuśćmy, że istnieje taki scenariusz:
Zbudowaliśmy system, działający pod obsługą PHP 5, naszym celem jest poszerzenie jego funkcjonalności o interfejs, którym pozwoli nam na dołączanie zewnętrznych rozszerzeń naszej aplikacji (ang. Plugins). Wtyczki w tym wypadku to pliki php zaprojektowane w odpowiedni sposób, tak aby implementowały odpowiednie metody i posiadały sprecyzowane wartości. Przeanalizujmy po kolei kroki, które musimy wykonać:
- wczytanie plików *.php z wybranej lokalizacji tj. folderu z wtyczkami
- iteracja na każdym pliku
- weryfikacja pliku jako w pełni funkcjonalnego rozszerzenia
- istnienie klasy o nazwie równej nazwie pliku (bez rozszerzenia)
- klasa dziedziczy po obiekcie Plugins
- zaimplementowane są następujące metody: load(), unload(), display()
- powyższe metody nie są ani chronione ani prywatne - muszą być publiczne
- w dokumentacji klasy zawarta jest informacja o autorze oraz wersji
Jeżeli powyższe kroki zostaną wykonane a warunki spełnione oraz plugin przejdzie pomyślnie proces weryfikacji, możemy załadować go do systemu. Wczytywanie plików z lokalizacji oraz iteracji po każdym z nich nie będę opisywał ze względu na łatwość implementacji oraz dostępność podobnych rozwiązań, podam jedynie przykładowy kod:
- $dir = 'plugins/';
- $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),
- RecursiveIteratorIterator::SELF_FIRST);
- foreach ($iterator as $file)
- {
- echo $file->getFilename;
- }
Blok kodu umieszczony w foreach będzie sercem naszej implementacji, to w nim będą następować operacje na obiektach zaimplementowanych przez wtyczki.
Reverse Engineering
Wykorzystamy do tego celu wspomniany wcześniej obiekt ReflectionClass oraz ReflectionMethod, fragment kodu, który sprawdza czy podany plik spełnia wszystkie wcześniej wymienione warunki aby być poprawnym rozszerzeniem.
- include($file->getPathname());
- /**
- * Nazwa pliku bez rozszerzenia jako nazwa klasy
- */
- $class = substr( $file->getFilename(), -4 );
- /**
- * Tablica z wymaganymi metodami
- */
- $aMethods = array('load', 'unload', 'display');
- /**
- * Zmienna określająca czy klasa jest poprawną wtyczką, jeżeli
- * tak to po przejściu poniższego bloku instrukcji powinna ona
- * przybrać wartość TRUE.
- */
- $is_valid = false;
- try
- {
- $refl = new ReflectionClass($class);
- /**
- * Nie wystąpił wyjątek a więc oznacza to, że w pliku istnieje klasa
- * o poprawnej nazwie. Warunek nr 1 spełniony.
- */
- $class_parent = $refl->getParentClass();
- /**
- * Sprawdzamy czy załadowana klasa dziedziczy po klasie 'Plugins'.
- * Jeżeli tak, warunek nr 2 spełniony.
- */
- if ( $class_parent->getName() == 'Plugins' )
- {
- $class_methods = $refl->getMethods();
- /**
- * Jeżeli licznik nie osiągnie wartości 3 - tylu funkcji oczekujemy,
- * oznacza to, że klasa nie posiada podstawowych i wymaganych
- * metod - load(), unload(), display() wymaganych aby spełnić
- * warunek nr 3.
- */
- $counter = 0;
- foreach ( $class_methods as $refobj )
- {
- $method = new ReflectionMethod($class, $refobj->name);
- /**
- * Pomijamy metody będące konstruktorami oraz destruktor.
- */
- if ( $class == $refobj->name || $class == '__construct' ||
- $class == '__destruct' )
- continue;
- /**
- * Nie interesują nas także metody prywatne, czy też chronione
- * do których nie możemy się odwołać spoza klasy. Jedynie
- * metody publiczne.
- */
- if ( !$method->isPublic() )
- continue;
- /**
- * Sprawdzamy czy nazwa funkcji znajduje się w tablicy
- * z wymaganymi metodami. Jeżeli tak, zwiększamy licznik.
- */
- if ( in_array($refobj->name, $aMethods) )
- $counter++;
- }
- if ( $counter == 3 )
- {
- /**
- * Teraz pozostało jedynie sprawdzenie czy dokumentacja dla
- * klasy zawiera wymagane elementy - autora oraz wersję.
- */
- $doc = $refl->getDocComment();
- /**
- * W tym momencie skończyła się praca na obiektach, trzeba
- * cofnąć się do źródeł cywilizacji i użyć wyrażeń regularnych.
- */
- $pattern = '/(?:@author|@version)(?:[\t|\s]+)([^*|@]+)?/im';
- preg_match_all($pattern, $doc, $result);
- /**
- * Sprawdzamy czy w ogóle cokolwiek zostało dopasowane do
- * wzorca - autor i wersja, jeżeli nie opis jest błędny.
- */
- if ( count($result) > 0 && isset($result[0][0]) &&
- isset($result[0][1] )
- {
- if ( strtolower( substr( $result[0][0], 0, 7 ) ) ==
- '@author' )
- {
- $doc_author = $result[1][0];
- $doc_version = $result[1][1];
- } else
- {
- $doc_author = $result[1][1];
- $doc_version = $result[1][0];
- }
- /**
- * Sprawdzamy czy autor oraz wersja nie są pustymi
- * polami.
- */
- if ( $doc_author && $doc_version )
- {
- /**
- * Wszystko jest w jak najlepszym porządku.
- * Zebrane dane są gotowe do użycia.
- */
- $is_valid = true;
- } else
- {
- /**
- * Informacje o autorze lub wersji okazały
- * się niepoprawne.
- */
- }
- }
- } else
- {
- /**
- * Klasa nie posiada 3 podstawowych metod, więc nie jest
- * prawidłowym rozszerzeniem.
- */
- }
- } else
- {
- /**
- * Klasa mimo, iż jest poprawnie nazwana nie dziedziczy po klasie
- * 'Plugins'. Przechodzimy więc do kolejnej iteracji.
- */
- continue;
- }
- } catch (Exception $e)
- {
- /**
- * Plugin nie posiada poprawnie nazwanej klasy, więc przechodzimy
- * do kolejnej iteracji tj. kolejnego pliku. Możliwe jest także,
- * iż któraś z pozostałych instrukcji w bloku try{} spowodowała błąd.
- */
- continue;
- }
- if ( $is_valid )
- {
- /**
- * Plik jest poprawnym rozszerzeniem naszego interfejsu.
- *
- * $file->getPathName() - nazwa pliku, wraz ze ścieżką
- * $class - nazwa klasy
- * $doc - opis klasy (całość komentarza, nie obrobiona)
- * $doc_author - autor plugin-u
- * $doc_version - wersja plugin-u
- */
- } else
- {
- /**
- * Plik nie jest poprawny i nie może zostać wykorzystany
- * jako plugin.
- */
- }
- ?>
Mam nadzieję, że powyższy praktyczny przykład rzucił więcej światła na problem związany z wykorzystaniem jednej z technologi oferowanych przez PHP 5. Kod był pisany od ręki mimo to mam nadzieję, że ustrzegłem się od jakichkolwiek uchybień i błędów.
Czy podczas czytania tego tekstu były rzeczy dla Ciebie nie zrozumiałe, zdające się być niepoprawne, masz jakiekolwiek zastrzeżenia? Zostaw informację w komentarzach lub skontaktuj się ze mną poprzez e-mail.
Pozostałe metody (nie dotyczy użycia Introspekcji)
Wykorzystanie Reflection API nie jest jedyną metodą na uzyskanie informacji z pliku odnośnie zawartych w nim klas, funkcji, metod danych klas czy też zmiennych. Można tego dokonać również przy użyciu standardowych metod, mam na myśli tutaj kwestie możliwe do wykonania także przy użyciu PHP 4.
Nie jestem tego w stanie lepiej przedstawić niż podając konkrety.
- @include($file->getPathname());
- $class = $file->getFilename();
- $oClass = new $class;
- /**
- * Sprawdzenie czy $class jest podklasą danej klasy.
- * W ten sposób można sprawdzić, czy klasa rozszerza
- * obiekt 'Plugins'.
- */
- if ( is_subclass_of( $oClass, 'Plugins' ) ) { /* extends */ }
- /**
- * Metody klasy pobieramy za pomocą funkcji.
- */
- get_class_methods($class);
No dobrze, wszystko zdaję się działać? Mamy już nazwę klasy, wiemy czy dziedziczy ona po klasie ‘Plugins’, jesteśmy również w posiadaniu tablicy z metodami danego obiektu. Pozostało nam sprawdzenie czy posiada zadane 3 metody i czy są one publiczne… i tutaj zaczynają się schody. Trzeba to zrobić trochę na około używając wyrażeń regularnych.
Zrobiłem to według kilku punktów:
- Usuwam z pliku wszystkie komentarze, aby wyeliminować ewentualne funkcje, klasy, które są ukryte w komentarzach a mogłyby zostać błędnie rozpoznane i załadowane w dalszej części.
- Następnie gdy już usunąłem 3 typy komentarzy (// # /**/) mogę dopasować funkcje do wzorca
- Sprawdzam również czy po nazwie obiektu następuje słowo “extends” i pobieram nazwę klasy, po której dziedziczy
Poniżej niezbędna lista wzorców:
- /**
- * Komentarze
- */
- $content = preg_replace('@/\*.*?\*/@ims', '', $content); /* comment */
- $content = preg_replace('@^[\s|\t]+#.*?$@ims', '', $content); # comment
- $content = preg_replace('@^[\s|\t]+//.*?$@ims', '', $content); // comment
- /**
- * Pobieramy całą zawartość klasy - do znacznika 'class' (druga klasa) lub
- * końca pliku '?>'.
- */
- preg_match_all(
- '@^[\s|\t]+class[\s|\t]+([A-Z0-9_]+)([^{]+)?.*?(?:class|\?\>)@ism'
- , $content, $aresult);
- /**
- * Sprawdzamy czy klasa dziedziczy po 'plugins'.
- */
- preg_match('@extends[\s|\t]+plugins@ism', $aresult[2][0] );
- /**
- * Pobieramy nazwy metod - jedynie te publiczne lub nieokreślone (PHP 4)
- */
- preg_match_all(
- '@^[\s|\t]+(?:function|public[\s\t]+function)[\s|\t]+([A-Z0-9_]+)@ims',
- $aresult[0][0], $amethods);
Metoda nie wykorzystująca Introspekcji jest czasochłonna, przede wszystkim dlatego, iż dużo jest manipulacji z obrabianiem zawartości pliku. Problem mogą sprawić także niedociągnięcia we wzorcach wykorzystywanych jak widać na każdym kroku. Stanowczo polecam migrację z PHP 4 do 5. Projektując i tworząc aplikacje liczy się wydajność i łatwość rozbudowy, tak więc to od Ciebie zależy czy kod, który stworzyłeś będzie czytelny i łatwy w rozbudowie dla potencjalnych następców. Jeżeli zdecydowałeś się wykorzystać drugą metodę związaną z wyrażeniami regularnymi, radzę dodać komentarze do wzorców - odpowiednio opisane będą łatwe do zrozumienia nawet dla laika.
Przedstawiony tekst nie wyczerpuje całego tematu związanego z Introspekcją, po więcej informacji odsyłam pod adres http://php.net/manual/en/language.oop5.reflection.php do manuala PHP.
Życzę przyjemnej i owocnej pracy.
karpowicz.net
gmail.com
Bardzo interesujący artykuł, na pewno wykorzystam introspekcję przy najbliższej okazji, dzięki takim tutorial’om o wiele łatwiej zrozumieć podstawy nowych mechanizmów w PHP. Oby tak dalej, czekam na więcej postów o tematyce związanej z PHP5 i pozdrawiam.