Introspekcja w PHP 5 (ang. Reflection)

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.

  1. class ReflectionClass implements Reflector
  2. {
  3.     public string getDocComment()
  4.     public ReflectionMethod getMethod(string name)
  5.     public ReflectionMethod[] getMethods()
  6.     public ReflectionClass getParentClass()
  7.     public bool isSubclassOf(ReflectionClass class)
  8.     public bool implementsInterface(string name)
  9. }
  1. class ReflectionMethod extends ReflectionFunction
  2. {
  3.     public bool isPublic()
  4.     public bool isPrivate()
  5.     public bool isProtected()
  6.     public string getName()
  7. }
  8. ?>

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ć:

  1. wczytanie plików *.php z wybranej lokalizacji tj. folderu z wtyczkami
  2. iteracja na każdym pliku
  3. 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:

  1. $dir = 'plugins/';
  2. $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),
  3.                                         RecursiveIteratorIterator::SELF_FIRST);
  4.  
  5. foreach ($iterator as $file)
  6. {
  7.     echo $file->getFilename;
  8. }

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.

  1. include($file->getPathname());
  2.  
  3. /**
  4. * Nazwa pliku bez rozszerzenia jako nazwa klasy
  5. */
  6. $class = substr( $file->getFilename(), -4 );
  7.  
  8. /**
  9. * Tablica z wymaganymi metodami
  10. */
  11. $aMethods = array('load', 'unload', 'display');
  12.  
  13. /**
  14. * Zmienna określająca czy klasa jest poprawną wtyczką, jeżeli
  15. * tak to po przejściu poniższego bloku instrukcji powinna ona
  16. * przybrać wartość TRUE.
  17. */
  18. $is_valid = false;
  19.  
  20. try
  21. {
  22.     $refl = new ReflectionClass($class);
  23.  
  24.     /**
  25.       * Nie wystąpił wyjątek a więc oznacza to, że w pliku istnieje klasa
  26.       * o poprawnej nazwie. Warunek nr 1 spełniony.
  27.       */
  28.  
  29.     $class_parent = $refl->getParentClass();
  30.  
  31.     /**
  32.       * Sprawdzamy czy załadowana klasa dziedziczy po klasie 'Plugins'.
  33.       * Jeżeli tak, warunek nr 2 spełniony.
  34.       */
  35.     if ( $class_parent->getName() == 'Plugins' )
  36.     {
  37.         $class_methods = $refl->getMethods();
  38.        
  39.         /**
  40.          * Jeżeli licznik nie osiągnie wartości 3 - tylu funkcji oczekujemy,
  41.          * oznacza to, że klasa nie posiada podstawowych i wymaganych
  42.          * metod - load(), unload(), display() wymaganych aby spełnić
  43.          * warunek nr 3.
  44.          */
  45.         $counter = 0;
  46.  
  47.         foreach ( $class_methods as $refobj )
  48.         {
  49.                 $method = new ReflectionMethod($class, $refobj->name);
  50.  
  51.                 /**
  52.                  * Pomijamy metody będące konstruktorami oraz destruktor.
  53.                  */
  54.                 if ( $class == $refobj->name || $class == '__construct' ||
  55.                      $class == '__destruct' )
  56.                 continue;
  57.                
  58.                 /**
  59.                  * Nie interesują nas także metody prywatne, czy też chronione
  60.                  * do których nie możemy się odwołać spoza klasy. Jedynie
  61.                  * metody publiczne.
  62.                  */
  63.                 if ( !$method->isPublic() )
  64.                 continue;
  65.  
  66.                 /**
  67.                  * Sprawdzamy czy nazwa funkcji znajduje się w tablicy
  68.                  * z wymaganymi metodami. Jeżeli tak, zwiększamy licznik.
  69.                  */
  70.                 if ( in_array($refobj->name, $aMethods) )
  71.                 $counter++;
  72.         }
  73.  
  74.         if ( $counter == 3 )
  75.         {
  76.                 /**
  77.                  * Teraz pozostało jedynie sprawdzenie czy dokumentacja dla
  78.                  * klasy zawiera wymagane elementy - autora oraz wersję.
  79.                  */
  80.                 $doc = $refl->getDocComment();
  81.  
  82.                 /**
  83.                  * W tym momencie skończyła się praca na obiektach, trzeba
  84.                  * cofnąć się do źródeł cywilizacji i użyć wyrażeń regularnych.
  85.                  */
  86.                 $pattern = '/(?:@author|@version)(?:[\t|\s]+)([^*|@]+)?/im';
  87.  
  88.                 preg_match_all($pattern, $doc, $result);
  89.  
  90.                 /**
  91.                  * Sprawdzamy czy w ogóle cokolwiek zostało dopasowane do
  92.                  * wzorca - autor i wersja, jeżeli nie opis jest błędny.
  93.                  */
  94.                 if ( count($result) > 0 && isset($result[0][0]) &&
  95.                      isset($result[0][1] )
  96.                 {
  97.                         if ( strtolower( substr( $result[0][0], 0, 7 ) ) ==
  98.                              '@author' )
  99.                         {
  100.                                 $doc_author = $result[1][0];
  101.                                 $doc_version = $result[1][1];
  102.                         } else
  103.                         {
  104.                                 $doc_author = $result[1][1];
  105.                                 $doc_version = $result[1][0];
  106.                         }
  107.  
  108.                         /**
  109.                          * Sprawdzamy czy autor oraz wersja nie są pustymi
  110.                          * polami.
  111.                          */
  112.                         if ( $doc_author && $doc_version  )
  113.                         {
  114.                                 /**
  115.                                  * Wszystko jest w jak najlepszym porządku.
  116.                                  * Zebrane dane są gotowe do użycia.
  117.                                  */
  118.                                 $is_valid = true;
  119.                         } else
  120.                         {
  121.                                 /**
  122.                                  * Informacje o autorze lub wersji okazały
  123.                                  * się niepoprawne.
  124.                                  */
  125.                         }
  126.                 }
  127.         } else
  128.         {
  129.                 /**
  130.                  * Klasa nie posiada 3 podstawowych metod, więc nie jest
  131.                  * prawidłowym rozszerzeniem.
  132.                  */
  133.         }
  134.     } else
  135.     {
  136.         /**
  137.          * Klasa mimo, iż jest poprawnie nazwana nie dziedziczy po klasie
  138.          * 'Plugins'. Przechodzimy więc do kolejnej iteracji.
  139.          */
  140.         continue;
  141.     }
  142.  
  143. } catch (Exception $e)
  144. {
  145.     /**
  146.      * Plugin nie posiada poprawnie nazwanej klasy, więc przechodzimy
  147.      * do kolejnej iteracji tj. kolejnego pliku. Możliwe jest także,
  148.      * iż któraś z pozostałych instrukcji w bloku try{} spowodowała błąd.
  149.      */
  150.     continue;
  151. }
  152.  
  153. if ( $is_valid )
  154. {
  155.         /**
  156.          * Plik jest poprawnym rozszerzeniem naszego interfejsu.
  157.          *
  158.          * $file->getPathName() - nazwa pliku, wraz ze ścieżką
  159.          * $class - nazwa klasy
  160.          * $doc   - opis klasy (całość komentarza, nie obrobiona)
  161.          * $doc_author  - autor plugin-u
  162.          * $doc_version - wersja plugin-u
  163.          */
  164. } else
  165. {
  166.         /**
  167.          * Plik nie jest poprawny i nie może zostać wykorzystany
  168.          * jako plugin.
  169.          */
  170. }
  171. ?>

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.

  1. @include($file->getPathname());
  2.  
  3. $class   = $file->getFilename();
  4. $oClass = new $class;
  5.  
  6. /**
  7. * Sprawdzenie czy $class jest podklasą danej klasy.
  8. * W ten sposób można sprawdzić, czy klasa rozszerza
  9. * obiekt 'Plugins'.
  10. */
  11. if ( is_subclass_of( $oClass, 'Plugins' ) ) { /* extends */ }
  12.  
  13. /**
  14. * Metody klasy pobieramy za pomocą funkcji.
  15. */
  16. 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:

  1. 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.
  2. Następnie gdy już usunąłem 3 typy komentarzy (// # /**/) mogę dopasować funkcje do wzorca
  3. 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:

  1. /**
  2. * Komentarze
  3. */
  4. $content = preg_replace('@/\*.*?\*/@ims', '', $content); /* comment */
  5. $content = preg_replace('@^[\s|\t]+#.*?$@ims', '', $content); # comment
  6. $content = preg_replace('@^[\s|\t]+//.*?$@ims', '', $content); // comment
  7.  
  8. /**
  9. * Pobieramy całą zawartość klasy - do znacznika 'class' (druga klasa) lub
  10. * końca pliku '?>'.
  11. */
  12. preg_match_all(
  13. '@^[\s|\t]+class[\s|\t]+([A-Z0-9_]+)([^{]+)?.*?(?:class|\?\>)@ism'
  14. , $content, $aresult);
  15.  
  16. /**
  17. * Sprawdzamy czy klasa dziedziczy po 'plugins'.
  18. */
  19. preg_match('@extends[\s|\t]+plugins@ism', $aresult[2][0] );
  20.  
  21. /**
  22. * Pobieramy nazwy metod - jedynie te publiczne lub nieokreślone (PHP 4)
  23. */
  24. preg_match_all(
  25. '@^[\s|\t]+(?:function|public[\s\t]+function)[\s|\t]+([A-Z0-9_]+)@ims',
  26. $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.

1 Response to “Introspekcja w PHP 5 (ang. Reflection)”


  1. Gravatar Icon 1 Jacek W

    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.

Leave a Reply






O mnie

  • Programista PHP
  • Zwolennik Open Source
  • Użytkownik Linuksa (Debian)
  • Capoerista
  • Miłośnik Anime
  • Maniak optymalizacji i wydajności

Kategorie