FOL9000

Der f9-RequestDispatcher

von

Der RequestDispatcher ist ein Front-Controller, der zusammen mit dem BeanContainer den Kern von f9 bildet. Er bildet den Kern, weil alle Web-Requests hier ankommen und von hier aus weiter verteilt werden. Im Folgenden möchte ich den RequestDispatcher näher beschreiben – als ersten Teil der Beschreibung des f9-Frameworks.

Ein wichtiger Hinweis gleich vorweg: Der RequestDispatcher ist entstanden auf der Basis
der EpiRoute-Klasse des Epiphany-Frameworks. Mittlerweile sind die Unterschiede recht groß, aber die Verwandtschaft ist nicht zu übersehen. Deshalb: Dank an Jaisen Mathai und Kevin Hornschemeier für die großartige Grundlage. Wer nach einem hervorragenden micro-Framework für php sucht, sollte sich Epiphany auf jeden Fall ansehen.

Und noch ein zweites: Den Code zum Artikel gibts unten zum Download.

Das Front-Controller-Pattern

Für alle, die nicht wissen, was ein Front-Controller ist und welche Rolle er in einer Web-Anwendung spielt, zunächst ein paar kurze Sätze zur Erläuterung.

Ein Front-Controller bekommt alle eingehenden Requests einer Web-Site zugeleitet. Als Dispatcher oder Verteiler analysiert er die URL und die Parameter und leitet die Requests auf der Grundlage dieser Analyse an einzelne Controller weiter. Die Zuordnung von Requests oder URLs zu Controllern geschieht über eine Reihe von Mustern oder Regeln.

Der Front-Controller ist also der zentrale Einstiegspunkt für die gesamte Kommunikation mit einer Web-Anwendung.

Die einzelnen Controller sind dann Funktionen oder Methoden, die den Request abarbeiten; z.B. indem sie Funktionen und Methoden einer Service-Ebene aufrufen oder gleich Datenbank-Abfragen etc. durchführen — was hier geschieht ist abhängig von der weiteren Architektur des Systems und gehört nicht mehr primär zur Controller-Ebene.

Wer mehr über Front-Controller erfahren möchte, muss im Netz nicht lange suchen. Nach wie vor eine der besten Quellen ist Martin Fowlers Buch zu Anwendungs-Architekturen:

Fowler, Martin (2003): Patterns of Enterprise Application Architecture. (Addison-Wesley)

Namespace

Als Teil des f9-Frameworks liegt die RequestDispatcher-Klasse im net\f9-Namespace. Will man sie benutzen, ist die entsprechende Klasse also zunächst per use einzubinden:

use net\f9\RequestDispatcher;

Der RequestDispatcher

Wie beschrieben ist der RequestDispatcher der Einstigespunkt für jede f9-Anwendung. Ohne Erweiterung oder Ableitung einer neuen Klasse kann er die eingehenden Requests an php-Funktionen und Klassen-Methoden verteilen. Durch Definition neuer Loader und Runner oder durch Ableitung neuer Klassen können einfach weitere Controller-Typen genutzt werden.

Umleitung auf den Dispatcher

Damit alle Requests auf den RequestDispatcher umgeleitet werden, muss der Webserver entsprechend konfiguriert sein. Beim Apache geschieht dies über die .htaccess-Datei.

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)\?*$ /index.php?__route__=/$1 [L,QSA]

Ggf. muss hier noch der Pfad angepasst werden. Nimmt man aber die Regel in Zeile vier als Beispiel, würden alle Requests auf die Datei index.php umgeleitet. In dieser Datei muss dann der RequestDispatcher seine Arbeit tun — dazu später mehr.

Dispatching

Der RequestDispatcher verteilt eingehende Requests an Controller und gibt weiter, was die Controller zurückgeben — egal, was es ist: Ein String, ein Objekt oder irgendetwas anderes. Der Dispatcher ist reiner Weichensteller; er sorgt dafür, dass Daten an die richtige Stelle gelangen, greift die Daten aber nicht an, d.h. er manipuliert die weiterzuleitenden Daten nicht.

Dabei wird wie folgt vorgegangen:

  • Wenn der Rückgabe-Wert des Controllers ein String ist und mit ‚redirect:‘ beginnt, wird versucht, einen entsprechenden Redirect durchzuführen.
  • Wenn kein View-Resolver definiert wurde, wird der Rückgabe-Wert des Controllers unverändert ausgegeben (per echo).
  • Wenn ein View-Resolver definiert wurde, wird der Rückgabe-Wert des Controllers dem View-Resolver übergeben — der View-Resolver muss dann selbst anhand dieses Datums einen passenden View finden. Findet der View-Resolver einen passenden View, wird dessen Ausgabe-Methode (render()) aufgerufen. Für diesen letzteren Fall sind Klassen vorgesehen, die zwischen Controllern, View-Resolvern etc. ausgetauscht werden können und die View-Namen und die anzuzeigenden Modell-Daten zusammenfassen (ModelAndView).

View, ViewResolver sowie ModelAndView sind weitere f9-Klassen; sie werden an anderer Stelle eingehend erklärt.

Die ini-Datei

Der RequestDispatcher verteilt die eingehenden Requests auf der Basis von Regeln, die in einer ini-Datei zentral definiert werden.

Die Werte für method und path sind für alle Controller-Typen zwingend. Alle weiteren Werte können variieren, je nachdem von welchem Typ der Controller ist. Die Angaben für Controller-Funktionen und Controller-Methoden werden im Folgenden beschrieben. Für andere Controller-Typen können beliebige weitere Werte hinzugefügt werden, vgl. dazu den Abschnitt über die Erweiterungsmöglichkeiten des RequestDispatchers.

Funktionen als Controller

Das folgende Beispiel zeigt einen exemplarischen Eintrag für eine Route in der ini-Datei, bei der php-Funktionen als Controller eingesetzt werden. GET-Requests der Form /pingopong werden an die Funktion my_func() weitergeleitet.

[example]
method = GET
path = "/pingopong"
type = function
function = my_func

Die Benennung des Abschnitts mit example ist irrelevant und hat für die Funktion der Routen-Definition keine Bedeutung; sie muss nur eindeutig sein.

Zunächst wird mit method angegeben, für welchen HTTP-Request-Typ die Definition gelten soll (hier: GET).

Der path-Eintrag gibt die Route an, für die ein Handler definiert wird. Hier sind Reguläre Ausdrücke erlaubt; innerhalb eines Regulären Ausdrucks können mit Gruppierungen Teile der Route als Parameter für den Controller erfasst werden. Damit ist es möglich, einen Controller für verschiedene Routen zu nutzen (dazu hier mehr).

Schließlich geben die beiden folgenden Angaben den Controller an, an den die Requests weitergeleitet werden sollen. Im Beispiel ist dies eine Funktion mit dem Namen my_func.

Objekt-Methoden als Controller

Neben der Weiterleitung an Funktionen ist beim RequestDispatcher die Weiterleitung an Klassen-Methoden möglich. Dies muss wie folgt angegeben werden:

[example]
method = GET
path = "/pong"
type = class
class = net\f9\examples\PongController
function = pong_method

Hier wird der GET-Request /pong an die Methode pong_method der Klasse net\f9\examples\PongController weitergeleitet; wann immer ein /pong-Request zum RequestDispatcher gelangt, wird eine Instanz der Klasse PongController erzeugt und deren Methode pong_method aufgerufen.

Reguläre Ausdrücke, Gruppierungen und Controller-Parameter

Über Reguläre Ausdrücke in der Pfad-Angabe können verschiedene Requests auf den gleichen Controller umgeleitet werden und Teile der URL als Parameter an den Controller übergeben werden.

Mit folgender Pfad-Angabe würden alle Requests, die mit /user beginnen, an den gleichen Controller weitergeleitet.

path = "/user/+"

In der Regel ist das, was auf /user folgt, wichtig für die weitere Verarbeitung des Requests (es könnte hier z.B. ein User-Name oder eine User-ID sein.). Deshalb ist es möglich, Teile der Route über Regex-Gruppierungen festzuhalten und als Parameter an die Controller zu übergeben.

Controller-Parameter

Angenommen, es wäre eine User-ID (Integer), so würde folgende Pfad-Angabe zunächst einen Controller für alle Requests definieren, bei denen auf /user ein Integer folgt, z.B /user/537849.

path = "/user/?(\d+)"

Durch die Gruppierung wird zudem die ID an den Controller als Parameter weitergegeben. Die Controller-Funktion oder -Methode müsste folglich so aussehen:

function do_sth_with_user($id) {
  //...
}

Das heißt: Alle in einem Regulären Ausdruck angegebenen Gruppen werden dem entsprechenden Controller als Parameter übergeben.

Zusätzlich bekommt die Controller-Methode den eigentlichen Request als letzten Parameter übergebenn.

Controller bubbling

Controller bubbling heißt, dass mehrere Controller für die gleiche Route definiert werden können. Die Controller werden der Reihe nach aufgerufen und ihre Rückgabe-Werte wie oben beschrieben behandelt.

Auf diese Weise lassen sich Controller verketten und sich wiederholende Aufgaben in spezialisierte Controller auslagern. Genutzt werden kann dies zum Beispiel für Debugging-Zwecke oder ein sehr einfaches Logging.

Sind mehrere Controller für eine Route definiert, werden die Controller aufgerufen, bis der erste true zurückgibt. In diesem Fall wird die Kette unterbrochen bzw. beendet und der Request gilt als abgearbeitet.

Das Prinzip das Bubbling ist bekannt vom Event-Bubbling (z.B. in JavaScript), wo für ein Event mehrere Handler definiert werden können und wie beim Controller-Bubbling die Aufruf-Kette durch den Rückgabe-Wert eines Handlers beendet werden kann.

Einfache Bespiele für Controller-Bubbling finden sich in den Beispiel-Anwendungen sowohl für Controller mit Funktionen als auch für Controller mit Klassen.

Loader und Runner – den RequestDispatcher erweitern

Schon der einfache Request-Dispatcher kann mit zwei Arten von Controllern umgehen: Funktionen und Objekt-Methoden. Der Request-Dispatcher kann zudem leicht um weitere Controller-Typen erweitert werden, weil für neue Typen Loader und Runner abgegeben werden können.

Die Aufgabe des Loaders ist es, die Angaben der ini-Datei zu analysieren und daraus die Informationen zusamenzubauen, die der Runner benötigt, wenn er einen Request abarbeitet. Einfacher – aber auch nicht ganz korrekt – könnte man sagen, dass der Loader den Controller lädt und der Runner ihn im Bedarfsfalle aufruft.

Loader

Ein neuer Loader wird über die Methode addLoader($name, $func) dem RequestDispatcher hinzugefügt. Dabei ist der erste Parameter der Name des neuen Loaders, der zweite Parameter eine Funktion, der die Daten eines Abschnitts der ini-Datei als assoziatives Array übergeben werden. Auf der Basis dieser Daten registriert die übergebene Funktion einen Handler für den in der ini-Datei angegebenen Request-Typ (GET, POST etc.) und für die dort angegeben Route. Benötigt ein spezieller Loader mehr Daten als für die beiden Basis-Loader des RequestDispatchers vorgesehen, können sie in der ini-Datei einfach hinzugefügt werden.

Das folgende Beispiel zeigt den Loader für php-Funktionen:


addLoader('function', function($route) {
  if ((isset($route['type']) 
    && $route['type']==='function') 
    && isset($route['function'])) {
      $method = strtolower($route['method']);
      RequestDispatcher::getInstance()->$method($route['path'], $route['function']);
  }
});

In Zeile sechs des Beispiels findet das eigentlich interessante statt. Hier wird — je nach http-Methode — eine der RequestDispatcher-Methoden get($route, $callback), post($route, $callback), put($route, $callback) oder delete($route, $callback) aufgerufen. Das heißt: Es wird für die jeweilige http-Methoden für eine gegebene Route ein Controller registriert.

Runner

Ist der Controller registriert, d.h. eine Funktion, muss angegeben werden, was passieren soll, wenn ein Controller tatsächlich benötigt wird. Für den Fall einer Funktion als Controller heißt das: Die Funktion muss mit den entsprechenden Argumenten aufgerufen werden.

Ähnlich wie beim Loader wird auch hier eine Funktion registriert, die diesen Job übernimmt. In unserem Beispiel-Falle also ein Funktion, die die Controller-Funktion aufruft:

addRunner('function', function($def, $arguments) {
  if (is_string($def['callback']) 
     && function_exists($def['callback'])) {
       return call_user_func_array($def['callback'], $arguments);
  }
  return null;
});

Damit sind Loader und Runner für Funktionen als Controller definiert. Der BeanAwareRequestDispatcher ist ein Request-Dispatcher, der den RequestDispatcher um einen weiteren Controller-Typ erweitert: Um Methoden von Klassen bzw. Objekten, die dem f9-Bean-Container ‚entnommen‘ werden. Auch diese Erweiterung geschieht über die Registrierung eines je neuen Loaders und Runners. In der ini-Datei werden diese über den Typ bean angegeben.

Beispiel

Das einfache RequestDispatcher-Beipiel zeigt den grundlegenden Einsatz des RequestDispatchers.

Wie beschrieben sorgt die .htaccess-Datei dafür, dass alle Requests auf die Datei index.php umgeleitet werden. (Die Datei könnte natürlich auch anders heißen.)

In dieser Datei wird nichts anderes gemacht, als mit

RequestDispatcher::getInstance()->dispatch();

den eingegangenen Request an den Dispatcher weiterzuleiten, damit er den passenden Controller ermittelt und den Request dorthin weiterleitet.

Zuvor muss jedoch die ini-Datei mit den Routen-Definitionen gelesen werden:

RequestDispatcher::getInstance()->load('routes.ini');

In der ini-Datei werden zwei Routen definiert; der Einfachheit halber soll aber für beide Routen die gleiche Funktion als Controller benutzt werden:

[hello]
method = GET
path = "/hallihallo"
type = function
function = hello

[tach]
method = GET
path = "/tach"
type = function
function = hello

Weitere Beispiele (u.a. zum Controller-Bubbling) finden sich in den Downloads.

Downloads

Der RequestDispatcher und die Beispiele können jeweils einzeln als Archiv runtergeladen werden. Das komplette f9-Framework habe ich noch nicht gang fertig — bis dahin müssen die einzelnen Archive ausreichen.

Zunächst der RequestDispatcher selbst:
[download id=“1″]

Das einfache, oben beschriebene Beispiel findet sich hier:
[download id=“2″]

Schließlich zwei Beispiele für das Controller-Bubbling.
Zunächst Controller-Bubbling mit Methoden als Controllern:
[download id=“3″]

Und als zweites Controller-Bubbling mit Funktionen als Controllern:
[download id=“4″]

Das komplette f9-Framework mit allen Klassen und Beispielen:
[download id=“12″]

Schluss

Ein Request-Dispatcher ist immer ein guter Ausgangspunkt (im wahrsten Sinne des Wortes) für eine Web-Anwendung. Ob der hier vorgestellte Dispatcher das auch ist, könnt Ihr selbst entscheiden. Für das f9-Framework stellt er jedenfalls eine Kern-Komponente dar; zu den anderen Komponenten demnächst mehr.

Kommentare sind geschlossen.