Umsetzung eines Pluginsystems (II)24 Jul

Von Julian am 24.07.2009. Es wurde ein Kommentar hinterlassen. Du kannst einen Kommentar hinterlassen oder trackbacken.

In Teil I zu “Umsetzung eines Pluginsystems” wurden theoretische Überlegungen zu einem Pluginsystem angegangen. Die eigentliche Hürde ist jetzt die Verwirklichung der Theorie in der Praxis.

Plugin patterns

Zunächst wird ein Plugin-Muster festgelegt. Was ist darunter zu verstehen?
Alle Plugins müssen bestimmte Merkmale aufweisen, an Hand denen man sie identifizieren und in das bestehende System einbauen kann. Völlig willkürliche “Plugins” (oder in diesem Fall einfach willkürliche Codes) kann eine Anwendung kaum sinnvoll in sich “aufnehmen”. Es muss also vorher festgelegt werden, welchen Regeln ein Plugin folgen muss – und daran muss sich sowohl der Anwendungs- als auch der Plugin-Entwickler halten.

Für die praktische Umsetzung meiner Worte, gebe ich einfach mal ein Beispiel, was Regeln das sein könnten:

  1. Alle Plugins befinden sich in einem Ordner (plugins/). Sie bestehen entweder aus einer Datei oder einem Ordner und sämtlichen darin vorkommenden Dateien.
  2. Besteht ein Plugin aus einem Ordner, heißt die eigentliche Plugin-beinhaltende Datei wie der Ordner
  3. Jedes Plugin besitzt eine informative Datei mit dem Namen plugin-metadata.xml (plugin ist dabei mit dem jeweiligen Pluginnamen zu ersetzen)
  4. Der Pluginname beginnt mit einem Großbuchstaben und darf danach nur Buchstaben, Unterstiche oder Zahlen beinhalten
  5. Innerhalb des Plugins darf keinerlei direkte Ausgabe erfolgen (was mit Hilfe von ob_start() verhindert wird)
  6. Ein Plugin besteht aus einer Reihe von Funktionen und/oder Funktionsaufrufen und Berechnungen

Um es nun zu ermöglichen, dass sich Plugins für bestimmte Events registrieren können, braucht es eine Reihe von Funktionen. Die Anwendung soll sich also merken, welche Plugins geladen wurden und welche Funktionen sie für welche Events bereitstellen.
Damit diese Informationen nicht völlig frei zugänglich für jedes Plugin sind, werden sie einfach in einer Sammel-Klasse gekapselt.

class PluginList {
	static protected $plugins = array();
	static protected $events = array();
 
	static public function loadPlugin(){}
	static public function registerEvent(){}
	static public function event(){} //trigger() / do() / _() / exe() / ..
}

Für diese exemplarische Umsetzung wird eignetlich keine Plugin-Liste benötigt, da einfach alle Plugins geladen werden sollten. Wichtig ist die Variable $events.

Ein Plugin einbinden

Jedes Plugin wird also über loadPlugin() geladen. Allerdings hätte das Plugin damit Zugriff auf das Innere der Plugin-List – schließlich muss das Plugin irgendwo eingebunden (require_once()ed) werden. Daher lege ich zusätzlich noch eine Funktion an, die diesen Schritt letztlich durchführt:

function load_plugin($plugin){
	$plugin = ucfirst($plugin);
	if(!preg_match("=^[A-Z]?[a-zA-Z_]{0,}$=" $plugin){
		return FALSE;
	}
 
	$plugin_file = 'plugins/'.$plugin.'.php'; // die Strings kann man noch durch Konstanten oder getter-Methoden/Funktionen ersetzen
	$metadata_file = 'plugins/'.$plugin.'-metadata.xml';
 
	if(!file_exists($plugin_file) || !file_exists($metadata_file)){
		/* plugin is no single file */
		$plugin_file = 'plugins/'.$plugin.'/'.$plugin.'.php'; //übrigens schreibt man für gewöhnlich statt einem Slash auch DIRECTORY_SEPARATOR ;)
		$metadata_file = 'plugins/'.$plugin.'/'.$plugin.'-metadata.xml';
 
		if(!file_exists($plugin_file) || !file_exists($metadata_file)){
			/* plugin does not exist */
			return FALSE;
		}
	}
 
	ob_start('buffer_delete');
	require_once($plugin_file);
	ob_end_clean();
}

Wer die Funktion nicht im einzelnen durchgehen möchte, der möge mir verzeihen und den nachfolgenden Abschnitt rasch überspringen.
In der ersten Zeile wird zunächst sichergestellt, dass unser Pluginname mit einem Großbuchstaben beginnt. Danach folgt eine Überprüfung des Namens mit Hilfe eines regulären Ausdrucks – je nach Festlegung der Namensregeln für Plugins übertrieben oder notwendig. Man sollte zumindest sichergehen, dass keine Sonderzeichen darin vorkommen.
Danach folgen die beiden Pfade für die notwendigen Dateien eines Plugins. Existieren sie nicht, besteht das Plugin vermutlich aus einem Ordner und muss in diesem eine entsprechend benannte Datei besitzen. Ist dem wiederum nicht so, ist das Plugin nicht vorhanden.
Schließlich startet die Ausgabepufferung und das Plugin wird eingebunden. Übrigens macht die Funktion buffer_delete() nichts anderes als die Ausgabe vollständig zu entfernen. An dieser Stelle kann man auch mit Plugin-Fehlerbehandlungen ansetzen.

Die Methode loadPlugin der Klasse PluginList ruft also load_plugin() auf. In unserem Beispiel spielt sie also nur eine Wrapper-Funktion, aber in anderen Fällen kann man sie durchaus brauchen. Beispielsweise speichert sie den Namen des Plugins in der Pluginliste $plugins.

Wer aufmerksam den Code studiert hat, hat vielleicht bemerkt, dass die Methoden von PluginList statisch sind. Normalerweise mache ich das ungern, aber in dem Fall hat es durchaus einen Grund: Die Plugins müssen trotz der Kapselung über eine Funktion auf Methoden der Klasse zugreifen können. Also muss ich entweder eine Instanz der PluginList-Klasse übergeben oder sie statisch als Singleton verwenden und somit gewährleisten, dass die Plugins Zugang zu ihr haben.

PluginList::event()

Was geschieht nun aber bei einem Aufruf von PluginList::event()?

static public function event($function, $params = array(), $class = FALSE){}
  1. Den Eventnamen anhand vom Funktionsnamen und dem optionalen Klassennamen festlegen
  2. Sicherstellen, dass $params ein Array ist
  3. Pre-Call und Parametermodifikation umsetzen
  4. Erneut sicherstellen, dass $params nach der Parametermodifikation ein Array ist
  5. Ermitteln, ob die Funktion $function aufgerufen werden soll, oder es eine Replace-Function gibt
  6. Das Ergebnis der Funktion in einer Variablen speichern
  7. Ergebnismodifikation und Post-Call umsetzen

Jetzt sind wir wieder bei den fünf Begriffen, die ich im ersten Teil bereits schon um mich geworfen habe. Dabei wollte ich noch eins erwähnen: Im Prinzip kann man die ersten beiden und die letzten beiden jeweils zusammenwerfen, da eine Parametermodifikation genau wie ein Pre-Call immer -vorher- und eine Ergebnismodifikation genau wie ein Post-Call -nacher- durchgeführt werden.
Warum also die Trennung? In einem umfangreichen Pluginsystem könnte man beispielsweise Berechtigungen einführen. Nicht jede anwendungsgegebene Funktion sollte von einem Plugin mit solcher Macht wie der Parametermodifikation gehooked werden können. An dieser Stelle könnte man solche Überprüfungen mit einbauen und bspw. nur Pre-Calls zulassen – also Funktionen, die nur aufgerufen werden, aber nicht die Parameter oder das Ergebnis der eigentlichen Funktion beeinflussen. Das nur am Rande.

Kommen wir zum ersten Punkt. PluginList::event() muss erkennen, welche Funktionen durch Plugins für dieses Event registriert wurden. Im ersten Augenblick könnte man denken, der Funktionsname $function reicht dazu aus. Allerdings – wie ich oben bereits erwähnt habe – fließt in diese Sache noch ein optionaler Klassenname mit ein. In Teil I haben wir schließlich festgelegt, über diese trigger-Funktion möglichst alles verwenden zu können und dazu gehören auch Methoden von Klassen (auch wenn es Schwierigkeiten mit neuen Objekten macht ..).
Aus diesem Grund folgt ein Eventname in meinem Beispiel folgendem Muster: class.method. Ist es eine Funktion ($class = FALSE), so wird daraus function.

2
3
4
5
6
7
8
9
10
11
12
if($class === FALSE){
	$event = $function;
}
else{
	if(is_object($class)){
		$event = get_class($class).'.'.$function;
	}
	else{
		$event = $class.'.'.$function;
	}
}

Wenn einem Event eine Funktion – egal welchen Typs – zugeordnet wurde, muss das in $events gespeichert worden sein – und zwar mit diesem Eventnamen als Index. Nach dem Sicherstellen von $params folgt also für jeden der fünf Modifikations-Typen eine Abfrage, ob entsprechende Funktionen registriert wurden:

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$params = is_array($params) ? $params : array();
 
/* pre-calls */
if(isset(self::$events[$event]['pre_call'])){
	//call all registered functions
}
 
/* param modifications */
if(isset(self::$events[$event]['param_mod'])){
	// ..
}
 
/* replace function */
if(isset(self::$events[$event]['replace'])){
	//do replace function / method
}
else{
	//do original function / method
}
 
/* result modifications */
if(isset(self::$events[$event]['result_mod'])){
	// ..
}
 
/* post-calls */
if(isset(self::$events[$event]['post_call'])){
	// ..
}

So wird die Methode am Ende wohl strukturiert und vom Inhalt abgesehen aussehen. Innerhalb jeder Bedingung folgen nun Schleifen, Überprüfungen und self::event()-Aufrufe – außer in der else-Bedingung mit dem Kommentar “do original function / method”. Damit ruft sich die Funktion in 5 Fällen selbst wieder auf und ermöglicht so eine unendliche Schachtelung von Funktionen. Alle Funktionen hooken einander und im Prinzip ist nur ein einziger Aufruf in der Anwendung selbst notwendig, um ein komplett fertiges System zusammenzusetzen – wäre aber natürlich unübersichtlich und nicht sonderlich sinnvoll.

In Teil III (oh ja, den gibts auch :P ) folgt schließlich die Vollendung meiner Beispielsumsetzung mit einem kleinen Download. Es fehlt nur noch die Vollendung der beschriebenen PluginList::event()-Methode, die registerEvent()-Methode und eine Implementierung der Klasse (sowie das Singleton-Pattern). Viel Spaß beim drüber-Nachdenken,

Julian

Ein Kommentar zu "Umsetzung eines Pluginsystems (II)"

  1. krisi sagt:

    Wow.

    Ordentliche Arbeit. Schade nur, dass dieser Artikel eineinhalb Wochen zu spät für mich kommt ;) . Ich hätte das für ein aktuelles Projekt sehr gut umsetzen können.

Hinterlasse einen Kommentar

© 2009 GlobalIndustry-Project Blog