Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

02 Oktober, 2007

Das Picora-Framework - Der Einstieg

TL;DR:

Picora bietet dem PHP-Programmierer eine minimalistische Grundstruktur für einfache bis mittlere Projekte an. Wer nicht eines der großen Brüder RubyOnRails, Symfony oder das ZEND-Framework verwenden will und auch nicht den vollen Funktionsumfang dieser Giganten nutzen will, findet mit Picora ein einfaches Framework für den schnellen Erfolg.

Was bietet Picora?

Das Framework stellt mehrere Konzepte bereit, um - ähnlich wie bei RubyOnRails - schnell bestimmte Standardaufgaben zu lösen. Das MVC-Pattern kann zur Trennung von HTML, Logik und Datenhaltung verwendet werden. Der Dispatcher verknüpft URIs mit dem entsprechenden Controller, der wiederum die definierten Views rendert. Über sog. Events kann auf Ereignisse wie bspw. das Eintragen eines Datensatzes oder das Umleiten eines Requests reagiert werden. Dies ermöglicht die einfache Erweiterung des Frameworks, falls die gegebenen Features nicht ausreichen.

Das Framework stellt zudem Helfer für Aufgaben wie Usermanagement, Textformatierung, Bildbearbeitung, ein Testframework usw. bereit, die wahlweise verwendet werden können.

Die Ordnerstruktur

Nachdem man sich auf der Picora-Seite ein neues Projekt angelegt, heruntergeladen und entpackt hat, liegt folgende Ordnerstruktur zugrunde:

Ordnerstruktur

Im classes-Verzeichnis befinden sich alle Picora-Klassen. Ich habe zur Standardauswahl noch die Klassen PicoraActiveRecord und PicoraTest eingebunden. Wer Erweiterungen zum Framework machen will, kann hier seine eigenen Klassen unterbringen.

In controllers befindet sich bis jetzt nur die Klasse ApplicationController welche von PicoraController abgeleitet wurde. Es empfiehlt sich, in dieser Klasse nur Methoden zu definieren, die in jedem Controller benötigt werden. Die Methode layout() wird bei jedem Seitenwechsel aufgerufen (mehr dazu später). Die Methode error() wird jedes mal aufgerufen, wenn der Dispatcher die gewünschte Methode nicht gefunden hat oder ein anderer Fehler vorliegt. Die welcome()-Methode ist nur zu Demonstrationszwecken enthalten und kann gelöscht werden.

Der Ordner models ist anfangs noch leer. Hierin werden die Models aufbewahrt. Der scratch-Ordner dient für kleine Tests und Snippets. Wer dies nicht braucht, kann diesen Ordner komplett löschen. Die Verzeichnisse scripts und styles stehen für JavaScript-Dateien und CSS-StyleSheets bereit.

Wer genau hinsieht, hat sicher schon bemerkt, das im Verzeichnis views für jede Methode im ApplicationController eine HTML-Datei mit PHP-Bereichen angelegt wurde. Hier werden alle Templates aufbewahrt. Es empfiehlt sich für jeden abgeleiteten Controller ein extra Unterverzeichnis anzulegen, um so Probleme mit gleichen Dateinamen auszuschließen.

Um Hauptverzeichnis befinden sich Konfigurationsdateien und die index.php. Da der Dispatcher in der Grundkonfiguration mit mod_rewrite des Apache Webservers arbeitet, wird die Datei .htaccess zwingend benötigt. Wer LigHTTPd verwendet, kann diese Datei löschen und folgendes in die lighttpd.conf im entsprechenden Host-Block aufnehmen:

server.error-handler-404 = "/error.html"
url.rewrite-once = (
    "^/(scripts|styles)/(.*)$" => "$1/$2",
    "^/(.*)$" => "index.php?__route__=$1"
)

Die Datei functions.php ist für statische Funktionen vorgesehen, die ohne das Klassenkonzept - wie bspw. die Helper-Funktionen - auskommen. Wer dies lieber in statischen Klassen definiert oder keine Verwendung dafür sieht, kann diese Datei zusammen mit schema.php löschen. Die Dateien index.php und config.php werden gleich besprochen.

Da ich die Klasse PicoraText.php eingebunden habe, um einfache UnitTests zu machen, habe ich mir im Hauptverzeichnis den Ordner tests angelegt.

Grundkonfiguration und Bugs patchen

Da der Hauptprogrammierer von Picora momentan nicht aktiv am Code arbeitet, habe ich die Fehler die ich gefunden habe selbst beseitigt und zu einem Patch zusammengepackt. Dieser Patch sollte unbedingt eingespielt werden, da sonst einige Dinge nicht richtig funktionieren. Um den Patch einzuspielen, genügt es die Datei picora.beta5.fixes2.aamu.diff in das Hauptverzeichnis zu legen und folgenden Befehl im selben Verzeichnis auszuführen:

patch -p1 < picora.beta5.fixes2.aamu.diff

Betrachten wir die Datei config.php etwas genauer. In ihr werden drei Konstanten definiert. Der Verwendungszweck für den SECRET_KEY ist leider unbekannt und kann gelöscht werden. Die Konstante BASE_URL legt die Domain und evtl. Port und Unterverzeichnis fest. Dieser Pfad darf keinen endenden Slash enthalten! Der CONNECTION_STRING enthält die Zugangsdaten für die Datenbank. Hier ein Beispiel:

define('BASE_URL','http://aaron-fischer.net);
define('CONNECTION_STRING','mysql://aaron:xxx@localhost/aaron-fischer);

Über den Aufruf PicoraDispatcher::addRoute() wird der Dispatcher mit den Pfaden gefüttert, um die Zuordnung Pfad <-> Controller-Methode herzustellen. Dies werden wir uns später genauer anschauen.

Die Datei index.php ist der zentrale Einstiegspunkt der kompletten Anwendung. Es werden die noch fehlenden Dateien und Ordner eingebunden und den Dispatcher gestartet. Diese Datei muss nicht mehr bearbeitet werden, der Rest übernimmt der Controller.

Das MVC-Konzept

Schauen wir uns zunächst einmal einen normalen Ablauf an, welche die PHP-Engine beim Anzeigen einer Seite nimmt: Die URL wird aufgerufen und vom Webserver auf index.php gemapped. Das index.php Script stellt die Datenbankverbindung her, bindet die Konfiguration ein und übergibt anschließend den aufzurufenden Pfad an dem Dispatcher. Dieser analysiert den Pfad und vergleicht ihn mit dem zuvor eingegebenen Routen (PicoraDispatcher::addRoute()). Wenn die übergebene Route in diesem Navigationsverzeichnis vorhanden ist, wird die Anfrage direkt an den passenden Controller weitergereicht und die angegebene Methode aufgerufen. Ein Beispiel: Wir rufen die Adresse http://aaron-fischer.net/search auf.

PicoraDispatcher::addRoute(
    '/suche', array('Application', 'search')
);

Die Anfrage wird an den ApplicationController weitergeleitet und die Methode search() aufgerufen. Diese könnte folgendermaßen aussehen:

public function search($params) {
    if (trim($params['search']) == '')
        self::redirect(array('Blog', 'index'));
    return self::render('views/searchResults.php', array(
        'results' => PicoraActiveRecord::findAll('Entry', 
            array(
                'where' => /* Some SQL here */
                'limit' => 100,
                ...
    ));
}

Der Übergabeparameter $params mag anfangs etwas verwirren. Dieser enthält evtl. übergebene POST/GET-Daten. Wenn also der Parameter search leer ist, leitet die Methode search() den Benutzer direkt zum BlogController um und ruft die Methode index() auf. Anderenfalls fordert der ApplicationController vom EntryModell (Zeile 5) Datensätze an und legt diese in der Template-Variable results ab. Anschließend wird das angegebene Template mit den übergebenen Template-variablen gerendet und zurückgegeben. Die Übergebenen Template-Variablen sind direkt im Template verfügbar. Ein Beispiel von der Datei views/searchResults.php könnte so aussehen:

<ul id="search_results">
<?php foreach ($results as $res): ?>
    <li>
        <?php echo $res->title ?></a>
        <!-- some other stuff with $res -->
    </li>
<?php endforeach; ?>
</ul>

Nachdem das Template gerendert wurde, wird es an die index()-Methode des selben Controllers weitergegeben. Diese Methode könnte in etwa so aussehen:

public function layout($main) {         
    return self::render('views/layout.php', array(
        'main' => $main,
    ))->display();
}

Die HTML-Ausgabe vom searchResults.php-Template wird der layout()-Methode als Parameter übergeben und kann wiederum im Haupt-Template eingebunden werden. Das gerenderte Haupt-Template aus der layout()-Methode wird im Browser ausgegeben.

Modells

Im Beispiel zuvor haben wir das Modell nicht richtig genutzt, wir haben es praktisch umgangen. Datenbankabfragen sollten möglichst immer im Modell selbst abgehandelt werden, nur so kommt das MVC-Konzept richtig zur Geltung. Um nun die Suchergebnisse in das Modell auszulagern, erstellen wir ein neues Entry-Modell.

class Entry extends PicoraActiveRecord {
    const TABLE_NAME = 'entries';

    public static function getSearchResults($searchString) {
        // Some more validation here
        $searchString = strip_tags($searchString);
        return self::findAll('Entry', array(
                'where' => 'title = "%$searchString%"',
                'limit' => 100,
                ...
    }
}

Dieses Modell ist mit der Datenbanktabelle entries verknüpft und kann Abfragen zu dieser Tabelle machen. Es ist natürlich möglich, Tabellen mit joins respektive Relationships zu verbinden. Dazu aber später mehr.

Das Entry-Modell kann nun im Controller mit Entry::getSearchResults() zur Datenbankabfrage verwendet werden.