Routing im Front Controller

Posts in this series
  1. Front Controller Pattern - Definition
  2. Routing im Front Controller

Durch Routing wird in einer Webanwendung definiert, welcher Request welchen Controller-Aufruf zur Folge hat. Das Routing wird innerhalb des Front Controllers vorgenommen.

Routing per .htaccess

Das klassische Weg, wenn festgelegt werden soll, welche Scripte bei einem bestimmten URL-Aufruf ausgeführt werden sollen, ist die Scripte direkt aufzurufen (z.B. index.php, single.php usw.). Bei dieser Variante ergibt sich das Problem, dass sie schlecht wartbar ist, wenn eine bestimmte URL irgendwann einmal auf ein anderes Script verweisen soll, da dann sämtliche Aufrufe angepasst werden müssen. Deshalb (und aus SEO-Gründen) verschob man das Verweisen der URLs auf bestimmte Scripte in die .htaccess, wo man mittels mod-rewrite die je nach URL aufzurufenden Scripte problemlos ändern kann. Dieses Vorgehen erzeugt allerdings Probleme, die durch den Einsatz eines Front Controllers gelöst werden können. Die .htaccess sollte deshalb nur verwendet werden, um sämtliche eingehende Requests auf den zentralen Einstiegspunkt einer Anwendung zu leiten: den Front Controller.
Beispiel-Code einer .htaccess beim Einsatz eines Front Controllers:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php

Routing mittels eigener Klasse

Wenn nun die .htaccess nicht mehr für Routing-Aufgaben verwendet wird, muss diese Aufgabe in die Anwendung verlagert werden. Der Front Controller ist ja unter anderem dafür zuständig, die zu einem Request passende Controller-Klasse aufzurufen. Ihm muss nun nur noch ein Ansprechpartner an die Hand gegeben werden, den er fragen kann, welcher Controller zu dem ankommenden Request passt: das ist der Router.

Ein einfaches Router-Interface könnte so aussehen:

interface RouterInterface {
  /**
  * @return Controller
  */
  public function getController (Request $request);
}

Der Router würde dann dem Front Controller übergeben und der Front Controller fragt diesen Router anschließend durch den Aufruf der getController()-Methode, welcher Controller zu dem übergebenen Request aufgerufen werden soll.

Aufgaben und Umsetzung eines Routers

Die meisten Router-Klassen, die von PHP-Frameworks mitgebracht werden, bieten ausschließlich die Möglichkeit über URL-Parameter zu routen. Das kann in manchen Fällen zu sehr einschränken, z.B. könnte es bei einem Checkout-Prozess (Weg vom Warenkorb zur endgültigen Bestellung) nötig sein, dass alle Schritte des Prozesses unter der gleichen URL aufrufbar sind. In diesem Fall müsste der Router auch prüfen können, welche Schritte vom Kunden bereits erledigt wurden.

Wichtig für einen Router finde ich auch, dass der aufzurufende Controller als Objekt referenziert wird. Bei den meisten Frameworks geschieht dies in Form eines Strings, z.B. in Symfony:

$collection = new RouteCollection();
$collection->add('blog_show', new Route('/blog/{slug}', array(
    '_controller' => 'AppBundle:Blog:show',
)));

Wenn Controller als Strings angegeben werden, funktionieren bestimmte IDE-Funktionen wie z.B. das Suchen, ob und wo eine bestimmte Controller-Klasse oder -methode eingesetzt wird, nicht mehr (weil es sich für die IDE ja nur um einen String handelt). Dies machen fast alle Frameworks so. Den einzigen Grund, den ich dafür finden konnte, ist, dass die unnötige Instanziierung aller Controller-Objekte zur Übergabe an den Router zu Recht vermieden werden soll.
Um dieses Problem zu umgehen, könnte der Router obige Strings mit einer Factory-Methode in die tatsächlichen Controller-Objekte umwandeln (damit die IDE sie finden kann). Mein Ansatz ist, dass man dem Router einen oder mehrere RequestResolver übergeben kann und diese dann die tatsächlich aufzurufenden Controller generieren. Ob die Routing-Informationen dabei dann direkt in PHP, in einer Konfigurationsdatei oder einer Datenbank definiert sind, spielt dann keine Rolle mehr.

class Router implements RouterInterface {
    protected $requestResolvers = [];
 
    /**
     * @param Request $request
     *
     * @return Controller
     * @throws RequestNotResolvableException
     */
    public function getController (Request $request) {
        foreach ($this->getRequestResolvers() as $requestResolver) {
            $controller = $requestResolver->findController($request);
 
            if ($controller !== null) {
                return $controller;
            }
        }
 
        throw new RequestNotResolvableException('Cannot handle request');
    }
 
    /**
     * @return RequestResolver[]
     */
    public function getRequestResolvers () {
        return $this->requestResolvers;
    }
 
    /**
     * @param RequestResolver $requestResolver
     */
    public function addRequestResolver (RequestResolver $requestResolver) {
        $this->requestResolvers[] = $requestResolver;
    }
}

Die RequestResolver-Klasse(n) würden dann außerhalb des Frameworks für die konkrete Anwendung geschrieben werden.

Zusammenfassung

Ein Router übernimmt in einer Webanwendung das Delegieren der Generierung des Antwort-Dokumentes, das zu dem ankommenden Request passt. Dazu wird vom Front Controller der Request an den Router übergeben, der ein dazu passendes, auszuführendes Controller-Objekt zurückgibt.

Posts in this series
  1. Front Controller Pattern - Definition
  2. Routing im Front Controller

Jan hat 152 Beiträge geschrieben

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>