Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

Zaawansowany mechanizm refleksji w PHP

Język PHP od wersji 5.0 posiada wbudowany mechanizm refleksji. Umożliwia on dokładne zbadanie dowolnego interfejsu, klasy, a nawet zwykłej funkcji.

Wśród opcji, jakie udostępniają nam twórcy PHP, znaleźć można funkcjonalności listujące właściwości klas, parametry przekazywane do ich metod, czy nawet numery linii, w których zawierają się deklaracje badanych elementów. Wydaje się, że mechanizm ten udostępnia kompletny zbiór operacji, jakie potrzebne są do rozłożenia dowolnego programu na czynniki pierwsze.

Po „zabawach” z Refleksjami oraz jej metodą getDocComment() poczułem jednak pewien niedosyt: skoro można pobrać treść komentarzy do metody lub klasy, czemu nie można zrobić tego samego z kodem PHP danego elementu?

Stworzyłem więc bibliotekę AdvancedReflection rozszerzającą mechanizm refleksji o możliwość wylistowania kodu źródłowego PHP badanego elementu.

Możliwości biblioteki

W swojej podstawowej formie biblioteka AdvancedReflection:

  • korzysta ze wszystkich wbudowanych w PHP mechanizmów refleksji
  • zwraca kod źródłowy deklaracji klasy, interfejsu, metody lub funkcji
  • zwraca kod źródłowy ciała klasy, interfejsu, metody lub funkcji
  • oblicza dokładne położenie (offset) deklaracji klasy, interfejsu, metody lub funkcji w pliku PHP
  • oblicza długość w bajtach badanego elementu
  • zwraca numer linii oraz kolumny w której zaczyna się deklaracja elementu
  • wspiera PHP 5.3 oraz namespace’y

Przykładowy wynik

Skąd pobrać?

Na chwilę obecną pliki źródłowe dostępne są jedynie na tym blogu, kliknij tutaj aby rozpocząć pobieranie biblioteki (plik ZIP, rozmiar 33Kb).

Teoria

O ile opisana funkcjonalność nie wydaje się być zbyt skomplikowana (w końcu PHP to język interpretowany, mamy więc dostęp do kodów źródłowych każdej metody i klasy), to algorytm odczytu współrzędnych kodu nie jest już taki prosty do stworzenia.

Co prawda klasy ReflectionClass, ReflectionMethod oraz ReflectionFunction udostępniają dwie metody getStartLine() oraz getEndLine(), to jednak są one wysoce nieprzydatne w momencie, gdy w jednej linii kodu znajdują się deklaracje co najmniej dwóch elementów tego samego typu.

Z powodu specyfiki języka PHP (mieszanka kodu HTML i PHP) nie możemy też w prosty sposób domyślić się, w którym znaku ostatniej linii znajduje się koniec deklaracji (np. jak rozpoznać która klamra ‘}’ zamyka metodę, jeśli w tej samej linii jest ich więcej?).

Musimy więc posłużyć się bardziej zaawansowanym algorytmem, rozbijającym plik źródłowy na pojedyncze tokeny języka PHP. Do tego celu służy właśnie funkcja token_get_all() modułu Tokenizer (domyślnie wbudowanego w język PHP).

Opis działania Tokenizera

Napisana przeze mnie klasa Tokenizer rozbija kod źródłowy PHP na tokeny w celu wyszukania wymaganych słów kluczowych oraz zlokalizowania kompletnych bloków kodu. Nazwa pliku, w którym znajduje się badany element, pobierana jest z informacji otrzymanych z oryginalnego mechanizmu refleksji (metoda ReflectionClass::getFileName()).

W tym momencie zaczyna się największa praca. Po przesłaniu do funkcji token_get_all() zawartości pliku PHP otrzymujemy zbiór zagnieżdżonych tablic z informacjami o danym tokenie (słowie kluczowym/operatorze/komentarzu, itp.). Liczba informacji którą musimy przetworzyć jest duża, a w dodatku nieusystematyzowana (czasami tag jest array’em, czasami tylko stringiem).

W tym celu ujednolicam wszystkie tokeny:

foreach($this->tokens as $index => $token) {
	$this->tokens[$index] = new CodeToken($token, $this->lineNumber, $this->charColumn, $this->globalPosition);
}

Gdzie CodeToken to obiektowy odpowiednik array’a opisującego token (z dodatkową właściwością $charColumn):

class CodeToken
{
	// Starting char position of this token (in the starting line of code).
	public $charColumn = 0;
	// Starting line number of this token.
	public $lineNumber = 0;
	// Starting char position of this token in the global sourcecode.
	public $globalPosition = 0;
	// Token content.
	public $content;
	// Token type.
	public $type;
	// Token type name.
	public $typeName;
}

W tym momencie zaczyna się żmudne przeczesywanie tablicy tokenów w poszukiwaniu trzech potrzebnych nam typów T_NAMESPACE, T_CLASS, oraz T_FUNCTION (kompletna lista opisująca tokeny dostępna jest tutaj). W celu uproszczenia algorytmu przyjmuję, że interfejsy (T_INTERFACE) także należą do typu T_CLASS.

Po odnalezieniu tokenu function, class lub namespace następuje zapamiętanie jego pozycji w pliku (nr linii, kolumny, oraz offset w pliku źródłowym), a także odczyt dalszej części kodu w poszukiwaniu klamer otwierających oraz zamykających dany element (ignorując wszystkie inne polecenia znalezione po drodze).

if($token->content === '{') {
	$this->openBraces++;
} else if($token->content === '}') {
	$this->openBraces--;
	foreach($this->currentScope as $type => $value) {
		if($value) {
			if($this->currentBraces[$type] === $this->openBraces) {
				$this->blocks[$type][$this->currentScope[$type]]->setLength($token);
				$this->currentBraces[$type] = 0;
				$this->currentScope[$type] = null;
			}
		}
	}
}

Po odnalezieniu klamry zamykającej badany element zapamiętywane jest jej położenie względem początku pliku, oraz numer linii i kolumny w kodzie PHP.

Warto przy tym wspomnieć, że niektóre słowa kluczowe nie wymagają posiadania klamer, są to np. namespace’y. W celu ich obsłużenia należy dodatkowo wykonać test, czy wyrażeniem występującym po nazwie przestrzeni nazw nie jest klamra, lecz inne słowo kluczowe.

Uważny obserwator dostrzeże jednak, że podczas poszukiwań wspomnianych słów kluczowych nie bierzemy pod uwagę modyfikatorów dostępu, czy też polecenia abstract (np. abstract public function test()).

Nie jest to przeoczenie, lecz celowe rozwiązanie: ze względów wydajnościowych oraz czytelności kodu modyfikatory te wyszukujemy dopiero po odnalezieniu tokena kluczowego (class, function, interface). W takim przypadku przeszukiwanie odbywa się w kierunku wstecznym, w celu zbadania, co znajduje się przed deklaracją danego elementu.

Oto fragment metody Tokenizer::findModifiers($parentToken):

$foundToken = null;
$key = key($this->tokens);
 
static $ignorable = array(T_DOC_COMMENT, T_COMMENT, T_WHITESPACE);
$possibilites = array(
	T_NAMESPACE => array(),
	T_CLASS => array_merge($ignorable, array(T_FINAL, T_ABSTRACT)),            
	T_FUNCTION => array_merge($ignorable, array(T_FINAL, T_ABSTRACT, T_PUBLIC, T_PRIVATE, T_PROTECTED)),            
);
 
prev($this->tokens);
 
while(($token = prev($this->tokens)) && in_array($token->type, $possibilites[$parentToken->type])) {
	if($token->type !== T_WHITESPACE) {
		$foundToken = $token;
	}
}

Ostatecznie metoda wyszukująca bloki kodu wygląda więc następująco:

protected function guessType() {
	$token = clone(current($this->tokens));
	do {
		if(in_array($token->type, array(T_NAMESPACE, T_INTERFACE, T_CLASS, T_FUNCTION))) {
			if($token->type === T_INTERFACE) {
				$token->type = T_CLASS;
			}
			$firstToken = $this->findModifiers($token);
			if(!$firstToken) {
				$firstToken = $token;
			}
 
			$name = $this->getName();
			$block = new CodeBlockItem($this->sourceCode, $firstToken, $token, $name, $this->currentScope);
 
			$this->blocks[$token->type][$name] = $block;
			$this->currentScope[$token->type] = $name;
			$this->currentBraces[$token->type] = $this->openBraces;
		}
	} while($token = $this->nextToken());
}

Jak widać wszystkie znalezione bloki kodu zapisywane są w obiektach klasy CodeBlockItem.

Przechowuje ona informacje o dokładnym położeniu elementu w pliku, jego zasięgu (dokładna nazwa pliku/namespace’u/klasy w której się znajduje), a także jego rozmiar w bajtach pozwalający na pobranie jego kodu źródłowego z pliku. Informacje te są cache‘owane w klasie Tokenizer, oraz wykorzystywane przez mechanizm refleksji.

Rozszerzenie mechanizmu refleksji

W tym celu wystarczy dziedziczyć po klasach ReflectionClass, ReflectionMethod oraz ReflectionFunction, dodając do nowych klas brakujące metody.

Dziedzicząc należy jednak pamiętać o tym, by nadpisać też oryginalne metody, które zwracają obiekty refleksji (np. ReflectionClass::getMethods(), lub ReflectionMethod::getDeclaringClass()) – muszą teraz zwracać instancje opierające się na nowym mechanizmie.

Oto przykład takiego nadpisania:

/**
 * Gets the declaring class for the reflected method. 
 * 
 * @return AdvancedReflectionClass 
 */
public function getDeclaringClass() {
	$class = parent::getDeclaringClass();
	return new AdvancedReflectionClass($class->getName()); 
}

…oraz przykładowa metoda getBody() rozszerzająca ReflectionClass o możliwość zwrócenia kodu źródłowego badanej klasy:

/**
 * Returns class body.
 *
 * @return string 
 */
public function getBody() {
	return Tokenizer::getInstance($this->getFileName())->getItemByReflection($this)->getBody();
}

Co dalej?

Stworzoną w ten sposób bibliotekę łatwa można rozbudować, np. o zwracanie dodatkowych współrzędnych klamry zamykającej, bądź też długość bloku kodu (w tej chwili opcja ta jest zaimplementowana w kodzie, lecz niedostępna z poziomu refleksji).

Zależnie od założeń projektowych kolejnym krokiem może być też podmiana kodów źródłowych dowolnej klasy w locie.

Dodaj odpowiedź