Facebook
Twitter
Google+
Kommentare
21

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.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

21 Comments

  1. 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.

    Reply
  2. 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).

    Reply
  3. 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. 😉

    Reply
  4. 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.

    Reply
  5. @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.

    Reply
  6. 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();
    }

    }

    Reply
  7. @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).

    Reply
  8. @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.

    Reply
  9. 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.

    Reply
  10. @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?

    Reply
  11. 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 😉

    Reply
  12. @Christian
    Herzlichen Dank, ein interessantes Stück Code. Allerdings stellt sich mir wenn ich das so lese sofort wieder die Frage: ist das schnell?

    Reply
  13. @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.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen