Annotation – Proof of Concept
Wie ihr ja wisst, bin ich gerade dabei das Testtool LiveTest fertig zu stellen. Wie immer soll das Projekt besonders toll werden und ich habe mir auch ein paar Dinge überlegt, wie man besonders hip wirken kann. Annotations sind momentan an Buzzword, was bei keinem Bingo fehlen darf, deswegen dachte ich mir: bau ich’s doch mal ein.
Für alle die nicht wissen, was Annotations sind, hier der Versuch einer Erklärung: Annotations sind Meta-Daten im Quelltext, die nicht vom Interpreter als Code gesehen werden. Das Programm selbst kann aber reagieren. In den meisten Fällen schreibt man einfach ein paar Anweisungen in den PHPDoc-Block einer Klasse/Methode.
Da LiveTest über einen Event-Dispatcher die meisten Erweiterungen einbindet, dachte ich mir, es sei eine gute Idee, die Events und die Listener über Annotation zusammenzubringen. Wie sieht das dann aus?
class EventListener { /** * @event LiveTest.Run.PreRun */ public function react( ) { } }
Jetzt wäre es toll, wenn man diese Klasse beim Dispatcher registrieren kann und er weiß sofort, auf welchen Event, nämlich LiveTest.Run.PreRun, reagieren kann. Jetzt habe ich mich einfach mal hingesetzt und habe einen Event-Dispatcher gebastelt, der das kann. Der Code soll nur ein Proof of Concept sein. Wenn man was wirklich wichtiges macht, dann sollte man mal nach Annotation-Frameworks ausschau halten.
public function registerListener(Listener $listener) { $reflectedListener = new \ReflectionClass($listener); foreach ($reflectedListener->getMethods() as $reflectedMethod) { if ($reflectedMethod->isPublic()) { $docComment = $reflectedMethod->getDocComment(); $annotationFound = (bool)preg_match('^@event(.*)^', $docComment, $matches); if ($annotationFound) { $eventName = str_replace(chr(13), '', $matches[1]); $eventName = str_replace(' ', '', $eventName); $listenerInfo = array('listener' => $listener,'method' => $reflectedMethod->getName()); $this->eventListenerMatrix[$eventName][] = $listenerInfo; } } } }
Eigentlich ist der Code ganz einfach. Über die Reflection-API holt man sich die PHPDoc-Blöcke der jeweiligen Methoden und schaut, ob das vielleicht ein @event drinnen ist. Falls ja, dann speichere das weg und merke es dir. Ist jetzt keine besonders tolle, sichere und was weiß ich nicht alles, Lösung. Trotzdem zeigt sie wie man auch mit PHP arbeiten kann.
Ich finde das sehr interessant, vor allem da ich mich mit Annotations noch garnicht auseinander gesetzt habe. Im Java-Umfeld laufen die einem ja ständig über den Weg bzw. bei PHPUnit und den @covers oder @expectedException Annotations.
Wenn das WIki ein wenig mehr befüllt ist würd ich mir das Tool auch mal anschauen.
Ja, Annotations sind ein Buzzword – bei mir allerdings ein Summen was eher unangenehm klingt! Annotations für Dokumentation oder als Hints für Tests oder andere „Meta“-Tools zu benutzen finde ich ja noch legitim. Im Gegensatz dazu, Annotations so zu benutzen, dass sie die Programlogik beeinflussen finde ich ziemlich fies, da man hier ja noch wieder eine weitere „Sprache“ einführt (Anti-Pattern/Code-Smell G1: Multiple Languages in One Source File).
Code-Smell? Blödsinn! Java macht das schon seit Jahren. Zend Framework benutzt es auch. Und ich natürlich auch seit Jahren.
Es ist keine zusätzliche Programmiersprache, sondern semantische, statische Eigenschaften für Funktionen oder Klassen. Damit hält man sich Framework-spezifischen Code vom Hals und den Quellcode schön sauber und lesbar.
Noch besser: es ist selbst-dokumentierend.
@Nils dein Quellcode funktioniert nicht. Du kannst den Kommentarblock in einer Reflection nicht lesen, wenn die Klasse die Reflection nicht selbst erstellt hat. Du brauchst einen Annotation-Parser.
Ich habe einen solchen geschrieben. Für beliebige Klassen ist definitiv nicht so 0815 wie du es beschreibst.
Du kannst ja bei mir in den Quellcode schauen, wenn du magst. 😉
Eigentlich ^sollte^ der Compiler/Interpreter (je nach Sprache) schon reagieren, was aber in PHP (bisher?) nicht möglich ist. Siehe auch http://wiki.php.net/rfc/annotations
Wieso nutzt du nicht einen der vorhanden Umsetzungen?
Statt:
$eventName = str_replace(chr(13), “, $matches[1]);
$eventName = str_replace(‚ ‚, “, $eventName);
würde ich ja:
$eventName = preg_replace(‚/\s/‘, “, $matches[1]);
schreiben.
Auf den „Performance-Benefit“ von str_replace kommt es in dieser Methode wohl nicht an.
@Tom: Wenn es nicht funktioniert, warum funktioniert es dann?
@Nils aus Sicherheitsgründen erlaubt dir PHP nicht den Zugriff auf die Kommentarblöcke, außer die Klasse erstellt die Reflection selbst – also von sich selbst. Es funktioniert somit nur im Spezielfall, jedoch nicht allgemein für alle Klassen.
@Tom: Kann es sein, dass das nicht stimmt? Mein Code wäre ja ein Gegenbeispiel zu deiner Aussage, oder?
Doch, doch, das stimmt schon. Lies dir mal die Bug-Reports und die Einschränkungen durch, die dazu existieren. Damals als es eingeführt wurde habe ich das direkt getestet. Es funktioniert leider nicht in allen Fällen. Deswegen hatte ich einen Fallback implementiert.
Hier ist der Workaround für ReflectionMethod, für ReflectionClass schaut’s ähnlich aus:
public function getDocComment() {
if (parent::getDocComment() === false) {
$file = file($this->getFileName());
$comment = „“;
for ($i = $this->getStartLine(); $i > 0; $i–)
{
$docComment = $file[$i] . $docComment;
if (preg_match(‚/^\s*\/\*\*/‘, $file[$i])) {
break;
}
}
return preg_replace(‚/^\s*(.*?\*\/).*/s‘, ‚$1‘, $docComment);
} else {
return parent::getDocComment();
}
}
Kann es sein dass dieser Artikel nicht im RSS Feed mit phm»networ enthalten ist?
@Tom
Mit „Sprache“ ist bei diesem Code-Smell nicht „Programmiersprache“ gemeint.
Und du sagst es ja selber: Es ist eine „semantische Eigenschaft einer Funktion“, das bedeutet wenn ich den Code lese, muss ich diese Semantik kennen um das Programm-Verhalten zu erfassen (siehe als Beispiel das „Flow3“ Framework).
@Tom
Welche Informationen(Einschränkungen/Bug-Reports) meinst du denn? Ich kann dazu nichts finden.
Annotations und PHP, da fällt mir immer gleich Addendum(http://code.google.com/p/addendum/) ein.
Ich finde Annotations legitim, wenn der Code dadurch lesbarer wird. Es wäre auch schön mal ein Beispiel zu sehen, das den Autoloader von PHP benutzt, aber ich meine da läuft man in so ein Henne-Ei Problem rein 😉
@Christian Grundsätzlich geht es nur, wenn der Parser vor Aufruf des Skriptes die Originaldatei geparst hat.
Es funktioniert leider überhaupt nicht, sobald ein Byte-Code-Cache eingesetzt wird. Einige frühere PHP-Versionen hatten Bugs auch, die das Laden verhindern. Wenn man selbst hostet nicht relevant – aber wenn man auf einen Massenhoster angewiesen ist eventuell schon. Vor PHP 5.1.3 gab es die Funktion überhaupt nicht und stabil benutzbar wurde sie erst in einer späteren 5.2.x Version. Sollte normalerweise kein Problem sein, sollte man im Zweifel aber halt prüfen.
Bestimmte Coding-Standards gehen gar nicht: zum Beispiel darf keine Leerzeile zwischen Kommentarende und Funktionsanfang sein. Auch Blockkommentare, bei denen ein Kommentar für mehrere Funktionen gilt, kann man nicht verwenden. Einige schreiben sogar, dass der Parser den DocBlock nicht mehr findet, wenn vor der Deklaration ein If-Block steht.
Für interne PHP-Funktionen geht es sowieso nicht.
Wenn man sauber arbeitet ist das alles kein Thema, aber im Büro haben wir einige spezielle Exemplare, die zum Beispiel gern den DocBlock für Datei und Klasse in einem Abwasch erledigen.
@Norbert stimmt – das ist ein Henne-Ei-Problem ^^ Ich persönlich cache deshalb die Reflections. Ich entscheide dann im Dispatcher anhand der Reflection, ob ich die dazu passende Klasse überhaupt lade.
@Ilja aha! Ja, so macht das Sinn.
Ich für meinen Teil benutze es um Actions im Controller ein Template zuzuordnen, ebenso wie Zugriffsrechte, Menüeinträge, oder Sprachdateien, die automatisch geladen werden sollen. Außerdem benutze ich es um Actions als Webservices zu exportieren.
PHPUnit benutzt es ebenfalls: für erwartete Exceptions oder zum Zuordnen von Data-Providern.
Feine Sache! Auf diese Weise kann man die Logik zentral vorhalten und der Quellcode bleibt sauber und unübersichtlich.
@Nils Sorry – ich bin durcheinander gekommen. Also eingeführt hatte ich es ursprünglich, weil es mit EAccelerator und anderen ByteCode-Compilern nicht funktioniert. Im Wesentlichen schaue ich, ob der Parser selbst den DocBlock findet und falls nicht, lade ich die Quelldatei und suche im Zwischenraum vor der Funktion rückwärts nach der Stelle, an welcher der DocBlock beginnt. Das funktioniert – außer für interne PHP-Funktionen – in jedem Fall absolut zuverlässig.
Ach @Nils – Danke! Durch Deinen Artikel bin ich heute auf die Idee gekommen, das parsen der Annotations auf Lazy-Loading umzustellen. Das hat die Performance und den Speicherverbrauch des Dispatchers deutlich reduziert.
Wenn du es also auch implementieren willst würde ich dir raten, es gleich so zu machen: vor allem bei ReflectionsMethod spart es ordentlich Performance, wenn man die Annotations erst parst, wenn danach gefragt wird.
@Tom
Mein erster gedanke: er denkt wahrscheinlich an entsprechende Byte-Code-Caches die den Doc Comment nicht mitkompiliert… voila. Meiner Meinung nach haben die gängigen Implementationen immer irgendwelche Haken (z.B. case sensitivity in Addendum etc). Mich würde ja interessieren welche Implementationen hier so bevorzugt werden, da ich schon seit einiger Zeit an einer eigenen Arbeite, mich aber ständig in irgendwelchen Details verliere (z.B. verschachtelte Arrays in den Parametern o.ä.). Würde gerne allenfalls andere Implementationen sehen (wie z.B. Addendum, oder die in Stubbles). Da meiner Meinung nach die Performance entscheidend ist sollten die Lösungen so einfach wie nur möglich sein!
Die Annotations erst zu laden wenn sie benötigt werden macht sowieso Sinn, es ist aber zumindest für mich sehr schwer zu sagen wo sich ein gewisses Caching lohnt, da eigene Implementationen das ganze teilweise langsamer machen als erneutes Erstellen.
Sinn machen die Annotations doch an vielen Orten… IOC/DI Container, ORM, Validation und so weiter!? Oder sehe ich das falsch?
Edit: Einen Parser zu schreiben ist in diesem Zusammenhang doch eher ein notwendiger Fallback als die zu präferierende Lösung (Wobei das durch die Token Funktionalitäten in php ja nicht soo komplex ist)? Für irgendwas hat man ja Coding Standards.
Sorry für die vielen Schreibfehler 😉
@Rüfenacht Michael
Ich habe letztes Jahr auch einen Annotation Parser implementiert. Das ganze lässt sich hier finden:
https://github.com/akkie/mohiva-framework/tree/master/src/com/mohiva/framework/core
Die Dokumentation findest du hier:
http://redmine.mohiva.com/projects/mohiva-framework/wiki/ReferenceGuideAnnotations
Ich habe mich dabei an dem Annotation Parser von Doctrine orientiert.
@Christian
Herzlichen Dank, ein interessantes Stück Code. Allerdings stellt sich mir wenn ich das so lese sofort wieder die Frage: ist das schnell?
@Michael
Naja der zusätzliche Parse-Vorgang kostet erst einmal Zeit. Wenn man aber einen Cache verwendet, was ich auf jeden Fall empfehlen würde, hat man beim 2. Aufruf nur den Overhead durch das laden der Annotation-Klassen. Damit kann ich leben.
Die Doctrine Annotation Library welche unter anderem auch für die Annotierung von Doctrine 2 Entities verwendet wird kann ich empfehlen. Schön einfach verwendbar und verfügt auch gleich über diverse Caching-Möglichkeiten (den APC-Cache finde ich besonders praktisch).
http://jwage.com/2010/08/02/doctrine-annotations-library/