Kristall Tutorial – Teil 3: Request und Dispatcher

Kommen wir jetzt zu einem der Kernstücke des Frameworks, dem dispatcher. Dieser ist dafür verantwortlich, die vom Benutzer eingegebene URL an einen bestimmten controller mit einer bestimmten Methode zu übergeben, welche dann ausgeführt wird.

Um das ganze einfach zu halten, werden wir erst mal nicht mit einer routing Tabelle arbeiten sondern erwarten den controller Namen ohne das Suffix ‘Controller’ sowie den Namen der Methode in der URL. Zusätzlich definieren wir uns noch einen Standard Controller sowie eine Standard Methode die ausgeführt wird, wenn der Controller Name oder der Methoden Name oder beides nicht in der URL vorhanden sind.

Die leere Klasse haben wir uns ja bereits angelegt, und müssen erst einmal dafür sorgen, dass diese in der Applikation initialisiert und aufgerufen wird.

lib/Kristall/Kristall.php
class Kristall
{
    // ...

    /**
     * stores the dispatcher
     * @var kcDispatcher
     */
    protected $dispatcher    = null;

    // ...

    /**
     * Runs the application
     */
    public function run()
    {
        $this->dispatcher->dispatch();
    }

    // ...

    /**
     * Creates and configures a new instance of Kristall and registers the autoloader
     *
     * @param array $configuration
     */
    protected function  __construct($configuration)
    {
        // ... 

        spl_autoload_register(array($autoloader, 'autoload'));

        $classname   = $this->getSetting('app.dispatcher.class', 'kcDispatcher');
        if(!class_exists($classname,true)){
            throw new Exception('Invalid configuration setting "app.dispatcher.class". This class does not exist.');
        }
        $dispatcher  = new $classname();
        if(!$dispatcher instanceof kcDispatcher){
            throw new Exception( 'Invalid configuration setting "app.dispatcher.class". '
                                .'Class must be an instance of kcDispatcher');
        }
        $this->dispatcher = $dispatcher;
    }

    // ... 
}

Ok, wir haben ein neues Attribut namens dispatcher angelegt und der Applikation eine run() Methode hinzugefügt, die den Aufruf an den dispatcher weitergibt. Klassische delegation.

Der Konstruktor ist nun ebenfalls um einige Zeilen reicher und initialisiert unser dispatcher Attribut. Und zwar so, dass wir die Möglichkeit haben in der Konfiguration eine andere dispatcher Klasse als den Standard zu definieren. Das hat den Vorteil, später die Applikations Klasse nicht mehr umschreiben zu müssen.
Natürlich muss in dem Fall geprüft werden, ob die angegebene Klasse tatsächlich existiert und ob sie von der Standard Klasse abgeleitet worden ist. Wir wollen ja sicher gehen, das die angegebene Klasse eine dispatch() Methode hat.

Den Request kapseln

Prinzipiell wäre es nicht unbedingt nötig, eine Klasse zu schreiben, die einen HTTP Request kapselt, jedoch bietet es sich an, verschiedene Methoden zu implementieren, die auf bestimmt Eigenschaften des Requests prüfen lassen. Beispielsweise welche HTTP Methode verwendet wurde, ob es sich um einen AJAX Request handelt und änliches.
Für den jetzigen Zeitpunkt brauchen wir lediglich eine Methode, die uns die Route aus der URL herauszieht.

lib/Kristall/kcRequest.php
class kcRequest
{
    /**
     * Returns the requested route
     * @return string
     */
    public function getRoute()
    {
        $route = '/';
        if (isset($_SERVER['PATH_INFO'])) $route = $_SERVER['PATH_INFO'];
        if (isset($_SERVER['ORIG_PATH_INFO']))
                $route = $_SERVER['ORIG_PATH_INFO'];

        return trim(strip_tags(urldecode($route)),'/');
    }
}

Hier bedienen wir uns der PATH_INFO bzw. ORIG_PATH_INFO Variablen, um einigermaßen saubere URLs zu erreichen, auch wenn mod_rewrite nicht verfügbar sein sollte.
Da es immer wieder Leute gibt, die böses im Schilde führen müssen wir dafür sorgen, dass die Route, die wir ins System zurückgeben sauber ist.

Das Kernstück

Kommen wir zum Kernstück unseres dispatchers. Das ermitteln des controllers und der Methode die ausgeführt werden soll sowie deren Aufruf.
Nochmal zur Erinnerung, eine URL mit der unser Framework etwas anfangen können soll muss eine der folgenden Variationen sein:

/index.php
Damit lösen wir die Standard Methode des Standard Controllers aus
/index.php/test/
Dies ruft die Standard Methode des Controllers ‚testController‘ auf.
/index.php/test/hallo
Führt zum aurfruf der Methode ‚executeHallo‘ im Controller ‚testController‘
lib/Kristall/kcDispatcher.php
class kcDispatcher
{

    /**
     * Tries to create a controller object from the given name. If the given name is empty, the default controller
     * will be used.
     * To overwrite the default controller, you can set the appropiate config variable.
     * @param string $name
     * @return kcController
     * @throws Exception
     */
    protected function getController($name)
    {
        if( 0 <strlen($name)){
            $controller = sprintf('%sController',$name);
        }
        else{
            $controller = sprintf('%sController',
                                  Kristall::application()->getSetting('app.defaults.controller', 'kcController'));
        }
        if(!class_exists($controller, true)){
                throw new Exception(sprintf('The requested controller %s does not exist',
                                            $controller));
        }

        return new $controller();
    }

    /**
     * Tries to find a matching action for the given name in the given controller. If the given name is empty, the
     * default action will be used.
     * To overwrite the default action, you can set the appropiate config variable.
     * @param string $name
     * @param kcController $controller
     * @return string
     */
    protected function getAction($name, $controller)
    {
        $default = Kristall::application()->getSetting('app.defaults.action', 'index');
        $action  = sprintf('execute%s', $default);
        if( 0 <strlen($name)){
            $action = sprintf('execute%s',ucfirst($name));
        }
        if(!method_exists($controller,$action)){
        throw new Exception(sprintf('The requested controller has no action %s',
                            $name));
        }
        return $action;
    }

    /**
     * Dispatches the request 
     */
    public function dispatch()
    {
        $request             = new kcRequest();
        list($name, $action) = explode('/', $request->getRoute());
        $controller          = $this->getController($name);
        call_user_func_array(array($controller, $this->getAction($action,$controller)),array($request));
    }
}

Sehen wir uns die Methoden im Detail an:
getController() erwartet einen Namen und prüft ob es eine Controller Klasse mit diesem Namen gibt. Ist der übergebene Name leer, wird der Standard Controller verwendet, den wir in der Konfiguration festlegen können.

getAction() erwartet einen Namen für die Methode, die aufgerufen werden soll und das Controller Objekt, das eine Methode mit diesem Namen haben soll. Ist der Übergebene Name leer, verwenden wir die Standard Methode, die ebenfalls wieder in der Konfiguration festgelegt werden kann.

dispatch() lässt sich nun aus der Route ein Controller Object und den Namen der Methode erzeugen und ruft dieses über einen callback auf.

Konfiguration erweitern

Es wird sicherlich nicht entgangen sein, dass wir nun zwei weitere Einträge in der Konfiguration machen können. Nämlich ‚app.defaults.controller‘ und ‚app.defaults.action‘. Da wir in unserer Applikation natürlich nicht direkt mit dem Basis-Controller arbeiten wollen, werden wir zumindest den conroller Namen in die Konfiguration schreiben und eine entsprechende Klasse im app/controller Verzeichnis anlegen.

app/config/config.php
return array(
    'app' => array(
        'defaults' => array(
          'controller'  => 'default',
        ),
        'autoload' => array(
            'cacheDir' => '',
            'useCache' => false,
        )
    )
);

Die standard Aktion ist, sofern nicht anders Konfiguriert, executeIndex() womit sich eigentlich ganz gut leben lässt.
Den Standard Controller haben wir jetzt auf ‚default‚ umgestellt, daher benötigen wir jetzt eine Klasse mit dem Namen defaultController die sich von unserem Basis-Controller ableitet und eine Methode namens executeIndex() hat.

app/controller/defaultController.php
class defaultController extends kcController
{
    public function  executeIndex(kcRequest $request)
    {
       echo 'hallo Welt!';    
    }
}

Ausgezeichnet!

Jetzt können wir endlich mal den Webserver und einen Browser starten und uns über ein gutes altes ‚hallo Welt!‘ freuen.

Damit hätten wir, zumindest im Groben, den Controller Teil abgehandelt. Als nächstes wenden wir uns der View Ebene zu.

3 Bemerkungen zu “Kristall Tutorial – Teil 3: Request und Dispatcher

  1. mig

    Moin,

    danke für dein gut verständliches Tutorial.
    Den Weg, den du da gehst finde ich sehr Interessant.

    Habe jedoch ein Problem mit dem kcAutoload …
    Ich bekomme durch den destruct
    file_put_contents …
    eine Fehlermeldung.
    Wenn ich den Ausklammer funktioniert alles.
    Finde da im Moment auch nicht den Fehler.

    Dann wäre da noch die spannende Frage, wann geht es weiter?

    Gruß, Mig

  2. Tom

    nachdem ich dann auch den Request um ienen default wert erweitert habe läuft nun alles 🙂

    Super Tutorial, wann gehts weiter?

  3. noxon

    hallo,
    ja, es ist sehr nett und verständlich beschrieben, danke dafür!
    schade das du nicht mit dem 4 teil weiter machst, wäre schon interessant zu sehen was dir noch so vorschwebt 😉
    vg,
    noxon

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.