Neue Möglichkeiten entdecken

Exar-Framework: Aspektorientierung mit PHP
Kommentare

Die aspektorientierte Programmierung führt in der PHP-Welt bisher ein Nischendasein. Deren Verwendung in anderen Programmiersprachen zeigt, dass die AOP bei geschicktem Einsatz die Wiederverwendbarkeit des Programmcodes deutlich steigern kann. Dieser Artikel zeigt die Möglichkeiten der Aspektorientierung für PHP-Anwendungen auf.

Ich bin ein Softwareentwickler. Ein leidenschaftlicher. Die Sprache meiner Wahl ist Java, meistens. Aber hin und wieder schreibe ich PHP-Anwendungen.

Wenn man mit mehreren Programmiersprachen parallel zu tun hat, lernt man in ihrer Gegenüberstellung sehr schnell Stärken und Schwächen der jeweiligen Sprache kennen. Beim Vergleich mit PHP fiel mir zuerst eine Stärke von Java besonders auf – die Möglichkeit der annotationsbasierten Entwicklung.

Von Java lernen

Wer mit JBoss Seam, Spring oder EJB gearbeitet hat, kennt die vielen Vorteile der annotationsbasierten Programmierung. Mithilfe von Annotationen werden Metainformationen in den Quelltext des Programms eingebunden und können vom Compiler, aber auch zur Laufzeit, ausgewertet werden, um Funktionalität der Anwendung zu beeinflussen. Annotationen bedienen in Java verschiedene Spezifikationen (APIs). So lassen sich beispielsweise mit JAXB Objektzustände sehr schnell und elegant in XML-Strukturen überführen, mit JAX-RS RESTful Web Services umsetzen oder mit JPA die Objekte auf eine relationale Datenbank übertragen. Dies alles sind Technologien, die es erlauben, innerhalb kürzester Zeit vollwertige Applikationen zu schreiben. Dabei bleibt der Quelltext sauber, denn die technischen Aspekte (wie z. B. die Bereitstellung einer Ressource als RESTful Web Service) werden durch externe Libraries umgesetzt und lediglich über die Annotationen mit dem Anwendungscode verknüpft.

Besonders interessant ist das in EJB 3.0 eingeführte „Standard Interceptor Model“. Mithilfe von Interceptoren kann man die Ausführung von Methoden abfangen und somit das Programmverhalten beeinflussen. Man spricht in diesem Zusammenhang auch von der aspektorientierten Programmierung, kurz AOP. Dies ist ein Programmierparadigma, bei dem Teile der Anwendung, die nicht unmittelbar zur Kernfunktionalität gehören, separat implementiert und erst zur Laufzeit in das eigentliche Programm eingefügt werden. Am Ende bleibt ein klar strukturierter, wartbarer Code, der mit Metainformationen versehen ist. Die Möglichkeit, dies annotationsbasiert zu machen, ist eine wesentliche Stärke von Java. Hier ist ein Beispiel einer annotierten Java-Klasse:

@Author(name = "John Doe")
class MyClass {
  ...
}

Wie nützlich wäre es doch, wenn man diese Technik auch auf PHP übertragen könnte. Doch dafür bietet PHP von Haus aus keine Möglichkeit. Im Jahr 2010 wurde ein entsprechender RFC abgelehnt. Die Realisierung von Annotationen als Sprachelement von PHP war damit vom Tisch. Trotzdem war der Wunsch in der Entwicklergemeinde groß, Metainformationen zu Methoden und Klassen direkt am Code zu hinterlegen. So begann man, dafür die Docblocks „zweckzuentfremden“. Die Annotationen konnten als Kommentar definiert werden, ein Parser hat sie zur Laufzeit ausgewertet und der Anwendung zur Verfügung gestellt. Das ist zwar weniger komfortabel als die direkte Unterstützung durch die Sprache selbst, aber immerhin eröffnete diese Vorgehensweise ganz neue Perspektiven in der PHP-Entwicklung. Plötzlich ließen sich auch mit PHP ganze Frameworks umsetzen, deren Techniken man aus der Java-Welt kannte. So entstand z. B. Doctrine 2 – ein an Hibernate angelehntes ORM-Framework, dass die Spezifikation des Persistenz-API (JPA) nach PHP portiert.

Ein anderes bekanntes Framework TYPO3 Flow geht da weiter und bietet AOP als Komplettpaket an. Damit kommt es der klassischen Vorstellung über die AOP am nächsten. Trotzdem stehen damit ein Paar wichtige Fragen im Raum:

  • Muss man unbedingt ein so großes und komplexes Framework einsetzen, wenn man eine überschaubare Applikation implementieren möchte?
  • Wie lassen sich AOP-Features in der eigenen PHP-Anwendung verwenden, wenn die Entscheidung über das zugrunde liegende Framework bereits feststeht? Kann man AOP-Funktionalität nachträglich in eine bestehende Anwendung einbauen?

Das alles macht deutlich, dass es einer neuen leichtgewichtigen Komponente bzw. Bibliothek für die Erfüllung dieser Anforderungen bedarf, die von den großen Frameworks nicht präzise genug adressiert werden.

Exar – ein AOP-Layer für PHP

Die Idee von einer kompakten AOP-Schicht für PHP war geboren. Diese Schicht sollte

  • AOP-Funktionalität über die annotationsbasierte Programmierung für PHP-Anwendungen in Anlehnung an Java zur Verfügung stellen.
  • ausschließlich auf der Standardfunktionalität von PHP aufbauen (d. h. keine zusätzlichen PHP-Erweiterungen voraussetzen).
  • möglichst wenige Abhängigkeiten zu Third-Party-Libraries aufweisen und keine besonderen Voraussetzungen an die Applikationsstruktur stellen.
  • einfach zu erweitern und in ihrer Verwendung optional sein, um die mit AOP einhergehenden Performanceverluste zu minimieren.

Als Grundlage braucht dieser AOP-Layer ein erweitertes ReflectionAPI, das Annotationen an PHP-Klassen und -Methoden verarbeiten kann. Die darauf aufbauende AOP-Komponente verarbeitet den annotierten Code und verknüpft diesen zur Laufzeit mit entsprechenden Aspekten.

So entstand Exar – ein schlankes Framework, das die von Java bekannte annotationsbasierte Entwicklung von PHP-Anwendungen ermöglicht. Ursprünglich war der Name eine Abkürzung von „Extended Application Repository“. Dabei ist es geblieben, auch wenn sich das Framework anders entwickelt hat, als sein Name es vermuten lässt.

Reflection++

Wie bereits weiter oben erwähnt, erweitert die Reflection-Komponente von Exar das Standard-Reflection-API um die Möglichkeit, Annotationen zu lesen. Eine Annotation wird dabei in Kommentaren hinterlegt. Listing 1 zeigt, wie Annotationen im Quelltext aussehen können.

Listing 1
/**
 * Diese Klasse nutzt verschiedene Annotationen.
 *
 * @NoValue
 * @FloatValue(1.5)
 * @StringValue(attr1='Attributwert 1', attr2='Attributwert 2')
 * @ArrayValue({'a', 'b', 'c'})
 */
class MyClass {
}

Jede Annotation beginnt mit @, gefolgt vom Annotationsnamen und den Attributen (in Klammern). Dabei wird die aus Java bekannte Schreibweise verwendet. Es wird zwischen drei Arten von Annotationen unterschieden:

  • Einfache Annotation: Annotationen ohne Attribute, z. B. @NoValue bzw. @NoValue().
  • Single-Value-Annotation: Annotation mit genau einem Attribut, z. B. @FloatValue(1.5). Das Attribut bekommt beim Parsen automatisch den Namen value. Im Beispiel könnte man auch @FloatValue(value=1.5) schreiben, was denselben Effekt hätte.
  • Multi-Value-Annotation: Annotation mit mehreren Attributen, die kommasepariert definiert werden, z. B. @StringValue(attr1=’Wert 1′, attr2=’Wert 2′). Die Attribute können auch optional sein.

Die Attribute können wiederum Zeichenketten, Zahlen, Arrays oder Konstanten (true, false, null) sein. Die Attributwerte werden von Exar automatisch in den richtigen Typ umgewandelt.

Es bleibt zu erwähnen, dass Multi-Value-Annotationen Attribute unterschiedlicher Typen definieren können, z. B. @MultiValues(attr1=’String‘, attr2=3.14, attr3={1, 2, 3}, attr4=false). Jede Annotation braucht außerdem eine eigene Kommentarzeile. Die so definierten Annotationen können nun mit dem Reflection-API von Exar ausgelesen werden. Dafür stellt das Framework die folgenden Klassen bereit: Exar\Reflection\ReflectionClass, Exar\Reflection\ReflectionMethod und Exar\Reflection\ReflectionProperty. Wir können die Annotationen der in Listing 1 deklarierten Klasse wie folgt auslesen:

$class = new \Exar\Reflection\ReflectionClass('MyClass');
$annotations = $class->getAnnotations(); // liefert ein Array mit vier Annotationsobjekten

Im erweiterten Reflection-API von Exar behandeln folgende vier Methoden die Annotationen in Klassen, Klassenvariablen und Methoden:

  • hasAnnotation() gibt zurück, ob die gegebene Annotation am Element definiert ist
  • getAnnotation() liefert die Annotation mit dem gegebenen Namen, die am Element definiert ist
  • getAnnotations() liefert ein Array mit Annotationsobjekten, die am Element definiert sind
  • getAnnotationMap() liefert ein assoziatives Array mit den Annotationen (mit Namen als Arrayschlüssel und den Annotationen als Wert)

Damit ist die Grundlage für die annotationsbasierte Entwicklung gelegt. Spannung in das ganze Thema bringt aber erst die AOP-Komponente.

AOP revisited

Wer in das Thema der Aspektorientierung einsteigt, wird zuerst mit einer Fülle an Begriffen konfrontiert: Aspect, Advice, Joinpoint, Pointcut oder Pointcut Expression. Um uns die komplexen Definitionen zu sparen, nehmen wir ein einfaches Beispiel zur Hilfe. Stellen wir uns vor, dass unsere PHP-Anwendung einen geschützten Bereich hat, in dem der Administrator Kundendaten anzeigen und editieren kann (Listing 2).

Listing 2
namespace Demo;
  
class AdminController {

  public function showCustomer () {
    // Nutzerdaten anzeigen
  }
 
  public function editCustomer() {
    // Nutzerdaten aktualisieren und speichern
  }

}

Um den Nutzerbereich gegen unberechtigte Zugriffe abzusichern, wird eine Basic-HTTP-Authentifizierung verwendet. Der Code dafür könnte aussehen wie in Listing 3.

Listing 3
public function showCustomer() {
  $loginSuccessful = false;
  if ($_SERVER['PHP_AUTH_USER'] == 'admin' && $_SERVER['PHP_AUTH_PW'] == 'secret') {
    $loginSuccessful = true;
  }

  if (!$loginSuccessful) {
    header('WWW-Authenticate: Basic realm="Administrationsbereich "');
    header('HTTP/1.0 401 Unauthorized');
    print "Login fehlgeschlagen!\n";
    exit();
  }
 
  // Nutzerdaten anzeigen
}

Es wird also geprüft, ob die globalen Auth-Variablen den Credentials „admin/secret“ entsprechen. Falls dies nicht der Fall ist, wird der Nutzer zur Eingabe seiner Login-Daten aufgefordert. Nun hat der Controller aber gleich zwei zu schützende Methoden. Normalerweise würde man an dieser Stelle die Credentials-Prüfung in eine neue Methode auslagern, wie in Listing 4 dargestellt.

Listing 4
namespace Demo;

class AdminController {

  public function showCustomer() {
    $this->checkCredentials();

    // Nutzerdaten anzeigen
  }
 
  public function editCustomer() {
    $this->checkCredentials();

    // Nutzerdaten aktualisieren und speichern
  }

  private function checkCredentials() {
    $loginSuccessful = false;
    if ($_SERVER['PHP_AUTH_USER'] == 'admin' && $_SERVER['PHP_AUTH_PW'] == 'secret') {
      $loginSuccessful = true;
    }

    if (!$loginSuccessful) {
      header('WWW-Authenticate: Basic realm="Administrationsbereich"');
      header('HTTP/1.0 401 Unauthorized');
      print "Login fehlgeschlagen!\n";
      exit();
    }
  }

}

Bei dieser Lösung gibt es jedoch ein entscheidendes Problem: Die Methode checkCredentials() ist privat und kann somit nicht in anderen Controllern verwendet werden. Man könnte sie zwar in einer eigenen Klasse kapseln und somit wiederverwendbar machen, aber die Aufrufe müssen weiterhin in jeder abzusichernden Controllermethode platziert werden. Dies würde jedoch bedeuten, dass die Geschäftslogik des Controllers mit den Sicherheitsaspekten der Anwendung vermischt wird. Denn die Methoden sollen die Kernfunktionalität bereitstellen, und die Prüfung der Login-Daten gehört nicht dazu und hat in einer solchen Methode nichts zu suchen.

Sicherheitsprüfungen im Code gehören typischerweise zu den Cross-Cutting-Concerns, also zu Funktionalitäten, die über den gesamten Code der Applikation verteilt sind. Das führt dazu, dass die Kernfunktionalität der Anwendung (Core Concerns) immer wieder mit weiteren, nicht zur Domäne gehörenden Funktionalitäten vermischt wird. Neben Security gehören zu Cross-Cutting-Concerns beispielsweise Logging, Tracing, Caching und Behandlung von Exceptions.

Genau dieses Problem adressiert die aspektorientierte Programmierung. Die AOP erlaubt die Ausführung von Aktionen in Form eines Codeabschnitts an verschiedenen Stellen in der Anwendung, ohne den dafür notwendigen Code zu verteilen oder redundant zu halten. Die Aktion (im AOP-Jargon heißt sie Advice) wird ausgelöst, sobald ein definierter Ereignispunkt im Code (Joinpoint) erreicht ist. Ein solcher Joinpoint kann an folgenden Stellen liegen:

  • vor dem Einstieg in eine Methode
  • nach der Ausführung der Methode
  • nachdem die Methode eine Exception geworfen hat

In unserem kleinen Beispiel mit dem Administrationsbereich wäre der Inhalt der Methode checkCredentials() die Aktion (bzw. Advice). Die Aktion muss vor dem Einstieg in die Methoden showCustomer() und editCustomer() ausgeführt werden. Dafür sorgt das Framework oder die Umgebung, in der die Applikation läuft.

Der Prozess des Verknüpfens der Advices mit dem Zielobjekt heißt Aspect Weaving. Man spricht an dieser Stelle vom so genannten „Weben“ der Klassen, das von der Weaver-Komponente erledigt wird (Abb. 1). In PHP werden die Klassen während des Autoloading-Prozesses gewoben. Zur Laufzeit arbeitet die Anwendung somit nicht mit ursprünglichen, sondern mit modifizierten Klassen, in die der Aspect-Code integriert ist. Nach außen bleibt die Schnittstelle dieser Klassen jedoch unverändert.

Abb. 1: Weaving in Exar

Abb. 1: Weaving in Exar

AOP in Exar

Mit Exar lässt sich das Problem der Vermischung von Funktionalitäten sehr elegant lösen. Exar verwendet hierfür Interceptor-Klassen, ähnlich wie Java das tut. Eine Interceptor-Klasse beinhaltet nicht nur den Advice-Code, sondern definiert auch, an welchen Joinpoints dieser Code auszuführen ist. Die Verknüpfung mit dem Anwendungscode erfolgt über Annotationen. So wird direkt an betroffenen Methoden festgelegt, welche zusätzliche Funktionalität über AOP mit dem Methodenaufruf ausgeführt wird. Für unser Beispiel sieht der Interceptor wie in Listing 5 aus.

Listing 5
namespace Demo\Interceptor;
 
use Exar\Annotation\Annotation;
use Exar\Aop\Interceptor\Interfaces\BeforeInvocationInterceptor;
use Exar\Aop\InvocationContext;
 
/**
 * @Target("method")
 */
class CheckCredentials extends Annotation implements BeforeInvocationInterceptor {

  public function beforeInvocation(InvocationContext $context) {
    $loginSuccessful = false;
    if ($_SERVER['PHP_AUTH_USER'] == 'admin' && $_SERVER['PHP_AUTH_PW'] == 'secret') {
      $loginSuccessful = true;
    }
 
    if (!$loginSuccessful) {
      header('WWW-Authenticate: Basic realm="Mein geschuetzter Bereich"');
      header('HTTP/1.0 401 Unauthorized');
      print "Login fehlgeschlagen!\n";
      exit();
    }
  }

}

Mit der Annotation @Target mit dem Wert method wird festgelegt, dass der Interceptor ausschließlich an Methoden platziert werden kann (andere mögliche Werte sind class für Klassenannotationen und property für Annotationen an Klassenvariablen). Der Interceptor muss die Klasse Exar\Aop\Annotation erweitern, um als solcher verwendet werden zu können. Das eigentlich Interessante befindet sich in der Methode beforeInvocation. Sie stammt aus dem Interface Exar\Aop\Interceptor\Interfaces\BeforeInvocationInterceptor und legt dadurch fest, dass dieser Interceptor beim Betreten der mit ihm annotierten Methode aufgerufen wird. Weitere Joinpoints werden durch folgende Interfaces realisiert:

  • Exar\Aop\Interceptor\Interfaces\AfterReturningInterceptor wird ausgeführt, nachdem die Methode ein Ergebnis geliefert hat (nach fehlerfreier Ausführung).
  • Exar\Aop\Interceptor\Interfaces\AfterThrowingInterceptor wird ausgeführt, falls beim Aufruf der Methode eine Exception aufgetreten ist.
  • Exar\Aop\Interceptor\Interfaces\AfterInvocationInterceptor wird nach dem Methodenaufruf ausgeführt (unabhängig vom Ausgang).

Somit ist der Prozess der Prüfung von Login-Daten zentral in einer eigenen Klasse gekapselt. Das ist die Controllerklasse, in welcher der Interceptor zum Einsatz kommt (Listing 6).

Listing 6
/**
 * @Exar
 */
class AdminController {

  /**
   * @CheckCredentials
   */
  public function showCustomer() {
    // Nutzerdaten anzeigen
  }
 
  /**
   * @CheckCredentials
   */
  public function editCustomer() {
    // Nutzerdaten aktualisieren und speichern
  }

}

Im Vergleich zur ersten Version in Listing 5 hat sich der Code der Klasse nicht verändert, bis auf die beiden Annotationen:

  • @Exar gibt dem Framework die Anweisung, die Klasse für die AOP-Funktionalität aufzubereiten. Dies geschieht während des Autoloading-Prozesses. Zum Einweben von Aspekten in den Anwendungscode greift Exar auf den PHP-Parser zurück.
  • @CheckCredentials verknüpft die Zielmethoden mit dem entsprechenden Interceptor anhand seines Klassennamens.

Der Code ist fertig. Um AOP in Aktion zu sehen, müssen wir Exar auf die Anwendung loslassen.

Die Einrichtung

Am einfachsten lässt sich Exar einrichten, wenn man es über Composer bezieht:

{
  "require": {
    "techdev-solutions/exar": "v0.1.0"
  },
  "minimum-stability": "dev"
}

Nach dem Installieren von Composer für das Projekt mit curl -s http://getcomposer.org/installer | php und dem Laden aller Abhängigkeiten mit php composer.phar install sind nur folgende vier Zeilen in die Startdatei index.php einzubinden:

require_once 'vendor/autoload.php';
set_include_path(dirname(__FILE__) . '/lib/' . PATH_SEPARATOR . get_include_path()); // Anwendungsklassen um Include Path hinzufügen
Exar\Autoloader::register(dirname(__FILE__) . '/_cache', array('Demo')); // Exar-Autoloader registrieren
Autoloader::addAnnotationNamespaces('\Demo\Interceptor');

Die erste Zeile lädt die Anwendungs-Dependencies, die in composer.json definiert sind. Nach dem Hinzufügen unserer eigenen Anwendungsklassen in Zeile 2 aktiviert die dritte Zeile Exar, indem der Exar-Autoloader registriert wird. Als Parameter wird der Pfad zum Cacheverzeichnis für Exar übergeben (dieses muss beschreibbar sein) sowie die Liste mit Namespaces, in denen die AOP-Features verwendet werden. Anschließend wird das Framework mit Autoloader::addAnnotationNamespaces informiert, in welchem Namespace der eigens geschriebene Interceptor zu finden ist.

Fazit

Die Vorteile von Exar und der AOP im Allgemeinen liegen auf der Hand. Die Kapselung von Cross-Cutting-Concerns in separaten Paketen führt zu einem Code, der ausschließlich die Kernfunktionalität beinhaltet. Zum einen erleichtert dies für den Entwickler den Einstieg in eine solche Applikation. Zum anderen lassen sich einmal implementierte Aspekte leicht von der Anwendung trennen und in einer andren Applikation wiederverwenden. Ähnlich zu Java könnte man ganze APIs annotationsbasiert umsetzen und sie als Bibliotheken verfügbar machen. Exar gibt dem Entwickler ein mächtiges Werkzeug, mit dem er die Funktionalitäten besser kapseln und dadurch die Applikationen sauber und wartbar machen kann. Dabei versteht sich das Framework als eine Schicht, die auf dem Anwendungscode liegt. Der Mehrwert von Exar entsteht erst, wenn man eigene Interceptor-Klassen implementiert und einbindet. Dafür stellt es auch keine weiteren Voraussetzungen an die Applikation. Die Anwendung kann weiterhin auf jedem beliebigen Framework aufsetzen, die Verwendung von AOP-Features mit Exar bleibt optional und unabhängig von der Applikationsstruktur.

Es soll aber auch nicht verschwiegen werden, dass es bei AOP auch Nachteile gibt. Der wohl wichtigste und am häufigsten genannte ist der Performanceverlust bei gewobenen Klassen. Es ist klar, dass das Hinzufügen zusätzlicher Funktionalität immer eine längere Ausführungszeit bedeutet. Man kann diesen Performanceverlust allerdings durch bestimmte Techniken eindämmen. So werden bei Exar nur die explizit markierten Klassen (über die Klassenannotation @Exar) in den Weaving-Prozess einbezogen. Außerdem werden die einmal gewobenen Klassen im Cache abgelegt und beim nächsten Aufruf aufbereitet geladen.

Sicherlich ist auch der Einwand berechtigt, dass AOP das Debuggen von Code erschwert. Es können Seiteneffekte entstehen, die das Verhalten des Programms zur Laufzeit weniger gut nachvollziehbar machen. Daher muss der Entwickler mit AOP-Techniken vertraut sein, bevor er diese einsetzt.

Letztendlich ist die Verwendung von AOP eine Entscheidung, die projektabhängig diskutiert werden sollte. Zweifelsfrei eröffnet die Aspektorientierung neue Möglichkeiten bei der Entwicklung. Ein Blick über den Tellerrand hinaus, auf die anderen Programmiersprachen und Frameworks, lohnt sich daher auf jeden Fall.

Aufmacherbild: abstract view to textured background of modern glass office building via Shutterstock.com / Urheberrecht: Vladitto

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -