Erweiterbarkeit ist eine zentrale Anforderung an nahezu jede Applikation. Dabei stellt man relativ schnell fest, dass dies auf verschiedenen Ebenen einer Anwendung passieren kann. Auf einer elementaren Ebene spiegelt sich die Erweiterbarkeit in der grundlegenden Architektur wieder, kann aber auch sehr hoch angesiedelt werden. Beispielsweise in einer Pluginschnittstelle für externe Programmierer.

Ralf beschreibt in seinem Buch eine gute Möglichkeit eine auf dem Zend Framework basierende Applikation modular zu strukturieren (mit Teilmodule, die nach belieben eingefügt oder ausgehängt werden können). Häufig ist aber auch das zu weit gefasst und eine viel klarere und schmalere Schnittstelle reicht vollkommen aus. Gesucht ist also ein Mechanismus, der Teilfunktionen dynamisch nachlädt und in die Gesamtapplikation einbindet.

Ein Beispiel: mein webbasierter RSS Reader rsslounge kann verschiedene Quellen verfolgen und verarbeiten. So liest er gewöhnliche RSS Feeds. Aber weiterführend kann er auch RSS Feeds als Quelle für Bilder verarbeiten (und extrahiert aus diesen RSS Feeds nur die Bilder). Möglich ist aber auch deviantArt als Datenquelle. Sprich jede mögliche Datenquelle ist denkbar. Nicht nur RSS Feeds, sondern auch normale HTML Seiten (die in irgend einer Weise geparst werden) oder auch der Zugriff auf einen Email Account wäre denkbar. Es macht also Sinn hier eine Abstraktionsebene einzubauen und die Möglichkeit zu schaffen einfach neue Datenquellen hinzuzufügen.

Die Vorgehensweise, welche ich für rsslounge gewählt habe ist simpel: jede Datenquelle ist eine Klasse, welche eine fest vorgegebene abstrakte Basisklasse (rsslounge_source) implementieren muss. Diese abstrakte Basisklasse gibt die Methoden vor, die unbedingt angeboten werden müssen (z.B. load() um die Datenquelle abzurufen).

abstract class rsslounge_source {
	abstract public function load($url);
	abstract public function getHtmlUrl();
	abstract public function getId();
	//...
}

Alle Datenquellen, welche die abstrakte Basisklasse implementieren, werden in einem festen Verzeichnis abgelegt. Die Gesamtapplikation durchsucht diesen Pfad nach verfügbare Datenquellen, prüft, ob sie die nötige Basisklasse implementieren und verwendet sie dann im Programm. Soll beispielsweise ein neues Feed hinzugefügt werden, so werden alle verfügbaren Datenquellen gesucht und im Dialog gelistet:

Feed hinzufügen

Angenommen ich entschließe mich ein Jahr später eine neue Datenquelle für die API von Google Analytics zu realisieren, so muss ich lediglich eine neue Klasse von rssloung_source ableiten, die entsprechenden Methoden implementieren und diese dann in das Verzeichnis ablegen.

Dieses Prinzip wird auch Hollywood Prinzip genannt: “Don’t call us, we call you”. Die Datenquelle Klasse bietet Methoden an, welche von der Gesamtapplikation im Bedarfsfall aufgerufen werden. Das Plugin muss von der Gesamtapplikation nichts wissen und ausschließlich ihre eigene Funktionalität und damit die Schnittstelle realisieren.

Die Implementierung basiert auf den Reflection Mechanismus der seit PHP 5 zur Verfügung steht. Mit Hilfe der Reflection Klassen können zur Laufzeit Klassen geladen und deren Eigenschaften ausgelesen werden. Zudem kann über diesen Weg die Klasse dann auch instanziiert werden:

$class = new ReflectionClass("plugin_rss_feed");
$baseClass = new ReflectionClass("rsslounge_source");
$class->isSubclassOf($baseClass); // true wenn plugin_rss_feed die Klasse rsslounge_source implementiert
$obj = $class->newInstance();

Sollen, wie im obigen Beispiel, alle verfügbaren Datenquellen gelistet werden, so wird das Verzeichnis mit den Datenquellen nach PHP Dateien durchsucht, geprüft ob diese von rsslounge_source abgeleitet sind und falls dies zutrifft instanziiert. Alle Objekte der so gefundenen Klassen werden in ein Array gespeichert und dann für die weitere Verarbeitung an die Gesamtapplikation übergeben (die diese Klassen dann auflistet):

$pluginLocation = "/plugins";
$pluginPrefix = "plugins_";
$plugins = array();
foreach(scandir($pluginLocation) as $file) {
	if(is_file($pluginLocation . "/" . $file)
	&& substr($file,0,1)!="."
	&& strpos($file,".php")!==false) {

		$classname = str_replace(".php","",$file);
		$class = new ReflectionClass($pluginPrefix.$classname);

		if($class->isSubclassOf(new ReflectionClass("base_plugin")))
			$plugins[$pluginPrefix.$classname] = $class->newInstance();
	}
}

Natürlich macht es hier Sinn einen Cache einzubauen, da sich die verfügbaren Klassen nicht oft ändern. Zend_Cache bietet hier eine einfache Möglichkeit so ein Array dateibasiert oder mittels memcached zu cachen.

Für meinen RSS Reader habe ich diesen Mechanismus zum Laden der Datenquellen in einen Action Helper ausgelagert. So steht diese Funktionalität in der gesamten Applikation zur Verfügung. Wer an Details interessiert ist, sei an dieser Stelle auf Google Code verwiesen, wo der Quellcode von rsslounge zu finden ist.

3 Kommentare zu “Pluginschnittstelle mittels PHP Reflection realisieren”

  1. Daff

    Ich bin mir manchmal nicht ganz sicher, ob die Entwicklung, die das Zend Framework in diese Richtung macht, wirklich so gut ist. Unbestritten, das PHP mit den OOP Features ungleich mächtiger geworden ist, aber die Zend Framework Entwickler scheinen mir manchmal etwas zu bemüht die Monster Design Patterns Java & Co zu adaptieren, die in einer dem Design nach dynamischen Sprache meiner Meinung nach oft überhaupt nichts zu suchen haben. Das soll wohl professioneller wirken, kommt mir aber oft so vor, als ob sie an dem eigentlichen Ziel die Stärken der Sprache auszuarbeiten vorbeischießen. Dein obiges Beispiel hätte ich einfach so gelöst:

    $classname = str_replace(“.php”,”",$file);
    $qualifiedname = $pluginPrefix.$classname;
    $plugins[$qualifiedname] = new $qualifiedname();

    Ok in deinem Fall müsste man wahrscheinlich noch prüfgen ob (new $qualifiedname() instanceof “base_plugin”).

    Als Gegenbeispiele zu dem was mich am Zend Framework stört fallen mir z.B. jQuery und Ruby on Rails ein. JavaScript war nie eine “schöne” Sprache aber mit jQuery drumrum kann man genau das machen wozu JavaScript gut ist und das macht dann auch noch Spaß. Ruby ist von der Syntax her ungewohnt und auch nicht sonderlich flott, trotzdem konnte es in Verbindung mit Rails Unmengen von Entwicklern begeistern.

    So, musste ich irgendwie mal loswerden, weil mich das beim Ausprobieren der aktuellsten Zend Framework Version ziemlich gestört hat ;)

     
  2. Tobi

    Hi David,

    da gehen die Geschmäcker ja bekanntlich auseinander (besonders bei der Aussage das JavaScript nie eine schöne Sprache war fällt mir direkt jemand ein, der dir da sofort widersprechen würde).

    Die Reflection Klassen werden durch PHP zur Verfügung gestellt und sind kein Bestandteil vom Zend Framework. Ich war auch skeptisch was die Notwendigkeit eines solchen Mechanismus in einer Skriptsprache angeht. Aber gerade für solche Aufgaben, wo zur Laufzeit die Eigenschaften von Klassen geprüft werden sollen, finde ich das einen sehr sauberen Weg. So könnte ich mir auch gut vorstellen einen MVC Front Controller auf diesen Weg zu implementieren. Dann könnte man beliebige Eigenschaften prüfen, z.B. ob eine Methode auch als public deklariert wurde.

    Was beispielsweise den Dispatch Mechanismus des Zend Frameworks angeht, gebe ich dir recht. Aber das dieser überladen und aufgebläht ist, ist ja mittlerweile bei den Entwickler angekommen. Laut Roadmap zur Version 2.0 soll sich hier auch einiges tun. Ich bin gespannt.

    Vielen Dank für deine Anmerkung! Finde es immer interessant alternative Wege zu sehen!

    Viele Grüße
    Tobi

     
  3. Daff

    Hm ok schön war vielleicht etwas unglücklich ausgedrückt. Je mehr ich mit JavaScript arbeite, desto mehr kann ich mich auch dafür begeistern, denn nur weil man mit einer Sprache – wie auch immer noch mit PHP – auch Pfusch machen kann (viel von dem Pre Web 2.0 JavaScript Sourcecode war einfach nicht “schön” und PHP war Anfangs auch nicht mehr als eine Hobby Webentwickler Skriptsprache) macht ja nicht unbedingt die Sprache selbst schlecht. Java zwingt einen ja im Endeffekt mehr dazu ein Programm ordentlicher zu gestalten, dafür büßt man aber auch sehr viel Flexibilität ein. Unter anderem deshalb bin ich gerade so begeistert von Groovy, wo man dynamische und statische Typisierung beliebig mischen kann.

     

Hinterlasse eine Antwort