Hätten Sie's gewusst? (Linux-Magazin, September 2008)

Das MVC-Framework Catalyst ist das ``Ruby on Rails'' der Perl-Welt. Bei der Entwicklung von Webapplikationen bietet es enormen Komfort und eine saubere Trennung der verschiedenen Komponenten.

Ob Spiegel-Online ``Reicht ihr Latein zum Angeben?'' ([2]) fragt oder food.aol.com die Leser durchgeschnittene Schokoriegel identifizieren lässt (Abbildung 1): so ein Online-Quiz ist ein schöner Zeitvertreib. Arbeitskollegen leiten die URLs auch gerne weiter und beim anschließenden Vergleichen der Punktzahlen entfalten sich oft anregende Diskussionen.

Abbildung 1: Welcher Schokoriegel ist das? Ein Quiz auf der Website food.aol.com ([3]).

Was liegt näher, als selbst so eine Quiz-Applikation zusammenzuklopfen? Abbildung 2 zeigt die heute vorgestellte Lösung in Aktion. Damit diese auch wiederverwertbar ist, soll sie die Quizfragen samt deren Multiple-Choice-Antworten als YAML-Datei (Abbildung 3) entgegennehmen. Das Beispiel zeigt eine Auswahl der Fragen der Einbürgerungsprüfung der USA. Zukünftige US-Staatsbürger müssen zum Beispiel wissen, wieviele Sterne auf der US-Flagge aufgedruckt sind und was diese symbolisieren ([5]). Die Webapplikation soll die YAML-Datei einlesen und die Fragen jeweils einzeln auf einer neuen Seite darstellen. Die YAML-Datei gibt die richtige Antwort stets als erstes, doch die Applikation soll später die Antworten zufällig durcheinanderwürfeln, damit der Test auch spannend bleibt.

Abbildung 2: Die mit Catalyst implementierte Quizapplikation in Aktion

Abbildung 3: Die Fragen mit Antworten liegen in einer YAML-Datei. Die erste Antwort ist jeweils die richtige.

Die Implementierung ist nicht sonderlich trickreich, aber es kommt schon so einiges zusammen: Schön designtes HTML mit dynamisch aufbereiteten Feldern, Session-Management zwischen den einzelnen Fragen, damit die Applikation die Punktzahl des Users nicht vergisst und am Ende eine Ergebnisseite, die dem Benutzer den Endstand mitteilt und zu einer weiteren Runde einlädt. Und schließlich darf der Server dem Client zu keinem Zeitpunkt über den Weg trauen, denn sonst könnte dieser schummeln.

Das Framework Catalyst unterstützt Perl-Programmierer bei derartigen Projekten, indem es automatisch ein Rohgerüst des Programmcodes erstellt, in das der Entwickler dann nur noch die Applikations-spezifischen Teile einfügen muss. Die Aufteilung der Komponenten in Model (Datenmodell), View (HTML-Darstellung) und Controller (Kontrollfluss) hat sich bei der Entwicklung von Webapplikationen bewährt und ermöglicht eine saubere Codetrennung und folglich leichte Wartbarkeit.

Abbildung 4: Am Ende angelangt erhält der User seine Punktzahl angezeigt.

Auf geht's beim Schichtl

Die Catalyst-Module liegen auf dem CPAN vor, wegen ihrer schieren Masse empfiehlt es sich aber, vorgefertigte Pakete zu installieren. Auf Debian-basierten Systemen sorgt

    sudo apt-get install \
      libcatalyst-perl libcatalyst-modules-perl

für die fachgerechte Installation aller notwendigen Module samt einem Rattenschwanz abhängiger Pakete. Damit der Entwickler nicht bei Adam und Eva anfangen muss, legt das mitgelieferte Skript

    $ catalyst.pl QuizShow

ein neues Verzeichnis QuizShow für die neu erstellte Applikation an und stellt dort auch noch etwa 30 Dateien in verschiedenen Unterverzeichnisse hinein, damit das ganze sofort betriebsfähig ist. Es finden sich unter anderem ein Makefile.PL, um die Applikation CPAN-gerecht zu verpacken, vordefinierte Konfigurationsdateien, Modulskelette zum Ausfüllen und diverse Skripts um neue Teile anzulegen und die Applikation auf unterschiedliche Weise zu starten.

Später lässt sich die Applikation als CGI-Skript oder unter mod_perl auf einem Apache-Server fahren, aber während der Entwicklung bietet es sich an, einfach den mitgelieferten Webserver zu starten:

    $ cd QuizShow
    $ script/quizshow_server.pl

Sofort fährt der Server hoch (Abbildung 5) und gibt akkurat formatierte Informationen über die Serverkonfiguration und den URL, unter der ein Browser ihn erreichen kann, bekannt. Die Standardeinstellung ist http://localhost:3000 und gibt man dies in einen Browser ein, zeigt dieser die Catalyst-Startseite an. In Produktionssystemen kommt später freilich ein Apache-Server zum Einsatz, aber in der Entwicklungsphase kommt der in Perl geschriebene Testserver wie gerufen.

Abbildung 5: Der im Catalyst-Paket mitgelieferte Testserver fährt hoch und gibt bereits implementierte Details der Anwendung bekannt.

HTTP mit Gedächtnis

Wenn ein Browser mit einem Webserver kommuniziert, behalten beide zwischen den einzelnen Requests keinerlei Zustand bei, falls man diesen nicht explizit mit Hilfe von Session-Cookies und serverseitig gespeicherten Sessiondaten sichert. Ein Quiz, das nach jeder Frage den aktuellen Punktestand vergäße, wäre wenig hilfreich, und so muss der Entwickler wohl in den sauren Apfel beißen und diese nicht ganz triviale Logik implementieren. Catalyst bietet allerdings schon ein vorgefertigtes Session-Management, ebenfalls als Debian-Paket an. Der Aufruf

    sudo apt-get install libcatalyst-plugin-session-fastmmap-perl

installiert die notwendigen Perl-Module. Damit die neu entwickelte Applikation andockenden Browsern automatisch beim ersten Kontakt ein Session-Cookie unterjubelt, mit diesem einen serverseitigen Speicher indiziert und dort Userdaten speichert, muss die Zeile

    use Catalyst qw/-Debug ConfigLoader Static::Simple/;

der vorher automatisch erzeugten Datei lib/QuizShow.pm zu

    use Catalyst qw/-Debug ConfigLoader Static::Simple
        Session Session::State::Cookie Session::Store::FastMmap/;

umgebaut werden. Damit kann eine Applikation jederzeit bequem mittels der Methode session() des Catalyst-Kontextobjektes auf den Sessionhash zugreifen. Dieser enthält die Sessiondaten im Key/Value-Format und wird von Catalyst automatisch unter der im Browsercookie gesetzten Session-ID gesichert und auf dem Server verwaltet. Dieses Verfahren funktioniert offensichtlich nur, falls der Browser bei jedem neuen Request immer mit demselben Server spricht (und nicht etwa mit einer zufälligen Instanz einer Serverfarm), doch für anspruchsvollere Konfigurationen bietet Catalyst auch datenbankbasierte Session-Lösungen an.

View

Als 'View', also als Anzeige-Komponente kommt wahlweise Perls Template-Toolkit zum Einsatz. Es definiert eine bewusst simpel gehaltene Template-Sprache, mit der der Anwender dynamische Felder in statischem HTML definiert. Sie erlaubt zwar auch einfache Programmlogik wie Bedingungen oder Schleifen, distanziert sich aber bewusst von anderen Lösungen, die die vollen Kapazitäten einer Skriptsprache anbieten. Der Grund: Allzu oft verführt dies unerfahrene Entwickler dazu, immer mehr Spaghetticode in die Darstellungsschicht einzuschleusen, statt die saubere Trennung von Kontrollfluss (Controller) und Darstellung (View) einzuhalten. Der Aufruf

    script/quizshow_create.pl view TT TT

des von Catalyst im Projektskelett mitgelieferten Skripts quizshow_create.pl fügt zum vorher erzeugten Dateibaum das Perl-Modul lib/QuizShow/View/TT.pm hinzu. Das erste ``TT'' steht für den Namen des zu erzeugenden Moduls (TT.pm), das zweite dafür, dass es sich bei letzterem um eine vom Template-Toolkit-View abgeleiteten Klasse handelt, denn Catalyst unterstützt auch noch Mason und und HTML::Template. Das Modul TT.pm macht macht Catalyst auch damit bekannt, dass Dateien mit der Endung .tt mit dem Template-Toolkit-Prozessor zu bearbeiten sind, bevor der Webserver sie ausliefert.

Abbildung 6: Das Template quiz.tt bestimmt das Erscheinungsbild der Applikation.

Abbildung 6 zeigt die Template-Datei quiz.tt, die im Verzeichnis root des neu erzeugten Catalyst-Projekts liegen muss. Die in Template-Toolkit-Lingo als [% IF %] geschriebene If-Bedingung prüft dort anfangs, ob keine weiteren Fragen vorliegen und ob deswegen der Endstand angezeigt werden soll. Falls nicht, wird oben im Browser der Spielstand mit den Template-Variablen score_ok und score_nok angezeigt und die Anzahl der noch ausstehenden Fragen. Dann gibt das Template die aktuelle Frage aus und iteriert mit einer FOREACH-Schleife über die in zufälliger Reihenfolge vorliegenden Antworten und gibt diese mit Radiobuttons zum Anklicken aus. Ein Submit-Button schickt das Web-Formular ab und kontaktiert den Webserver wegen einer fehlenden URL einfach wieder dem ursprünglichen URL.

Kontrolliere den Fluss

Um den Kontrollfluss der Applikation zu definieren, muss auch noch ein Controller Quiz.pm her:

    script/quizshow_create.pl controller Quiz

Dies erzeugt die Datei lib/QuizShow/Controller/Quiz.pm, die vom Entwickler mit dem in Listing Ctrl-Quiz.pm gezeigten Code erweitert wird.

Quiz.pm definiert die Methode quiz(), die mit dem Attribut :Global versehen ist. In dieser Einstellung fängt Catalyst alle Requests unter der URL http://localhost:3000/quiz ab und gibt etwaige weitergehende Pfade im Parameter @args an die Applikation weiter. Ruft der Benutzer zum Beispiel /quiz/reset auf, leitet Catalyst dies ebenfalls an die Methode quiz() weiter und setzt das erste Element von @args auf ``reset''.

In diesem Fall setzt quiz() die Session-Daten zurück auf Null und lässt den Browser einen Redirect auf die Startseite der Applikation ausführen. So wandelt sich die im Browser angezeigte URL wieder von /quiz/reset auf /quiz, der Controller setzt die Zähler für falsche und richtige Antworten wieder auf Null zurück und ein neues Quiz kann beginnen. Die Methode uri_for() des Catalyst-Objektes generiert aus relativ zur Applikationswurzel angegebenen URLs vollständige URLs, auf die ein dann Browser einen Redirekt ausführen kann. Die redirect()-Methode selbst setzt nur einen HTTP-Header aber unterbricht den Kontrollfluss nicht, sodass es wichtig ist, ein $c->detach() nachfolgen zu lassen, das Catalyst dazu veranlasst, den gerade bearbeiteten Request abzubrechen. Übrigens eine sehr praktische Methode, den Kontrollfluss abzubrechen, selbst wenn man sich gerade in einem verschachtelten Schleifenkonstrukt befindet. Die Variable $c zeigt auf das Kontext-Objekt des Catalyst-Systems, wird den Controllermethoden beim Aufruf beigepackt und eignet sich dafür, so ziemlich alles in den Untiefen des Catalyst-Systems Verborgene hervorzuholen.

Den durch Cookies und serverseitige Speicherung persistent gemachte Session-Hash bringt die methode session() des Catalyst-Objektes $c zum Vorschein. Er führt die Einträge next_question (Index der nächsten zu stellenden Frage im YAML-Array), score_ok (Anzahl der richtig beantworteten Fragen), score_nok (Anzahl der nicht richtig beantworteten Fragen), total (Gesamtzahl der Fragen) und correct_answer, damit der Server weiß, welche der auf der Webseite durcheinandergewürfelten Antworten nun die richtige für die gerade gestellte Frage ist.

Mit $c->req->param("answer") holt Catalyst den vom Browser geschickten Formular-Parameter 'answer' aus dem Request-Objekt. Diese Zahl entspricht der Nummer 1, 2 oder 3 des vom Benutzer aktivierten Radio-Buttons, mit dem er eine Antwort ausgewählt hat. Stimmt dieser Wert mit der vor der Auslieferung der Webseite im Session-Hash auf dem Server hinterlegten Wert überein, war die Antwort richtig und der Controller zählt die Session-Variable score_ok um eins hoch.

Spickzettel im Sessionhash

In Zeile 43 legt der Controller das zu verwendende Template als quiz.tt fest und muss anschließend die Werte der im Template verwendeten Variablen im sogenannten ``Stash'' festlegen. Setzt der Controller beispielsweise $c->stash->{score_ok}, so wird der Template-Prozessor den Eintrag [% score_ok %] im Template mit dem vom Controller gesetzten Wert ersetzen. Stash-Variablen können beliebig verschachtelte Datenstrukturen sein, so enthält der Stash-Eintrag answers zum Beispiel eine Referenz auf einen Array, dessen Elemente wiederum Referenzen auf Hashes sind, die unter den Keys text und num den Text und die Nummer einer Antwort enthalten. Das Template quiz.tt iteriert zur Darstellung über diesen Array, weist dem jeweils bearbeiteten Element den Alias answer zu und greift dann mit [% answer.text %] und [% answer.num %] auf die dahinter versteckten Hasheinträge zu: Eine sehr praktische Eigenschaft des Template-Toolkits, die viel Tipparbeit spart.

Die Zeilen 50 bis 52 bauen aus dem aus der YAML-Datei extrahierten Antworten-Array eine Datenstruktur, die der ersten Antwort den Eintrag ``correct'' zuweist und allen weiteren den Wert ``incorrect''.

Um die Antworten in zufälliger Reihenfolge darzustellen, holt die while-Schleife ab Zeile 57 ein zufälliges Element aus diesem Array von Arrays hervor. Ist es die vorher als korrekt markierte Antwort, merkt sich der Controller deren Nummer für später im Session-Hash. Zeile 61 macht aus der Antwort einen Hash mit den Einträgen text und num und schiebt diesen ans Ende des Arrays answers im Stash. Von dort holt das Template quiz.tt die Daten ab und erzeugt dynamisch das rausgehende HTML.

Listing 1: Ctrl-Quiz.pm

    01 ###########################################
    02 package QuizShow::Controller::Quiz;
    03 # Mike Schilli, 2008 (m@perlmeister.com)
    04 ###########################################
    05 use strict;
    06 use warnings;
    07 use base 'Catalyst::Controller';
    08 
    09 ###########################################
    10 sub quiz : Global {
    11 ###########################################
    12   my ( $self, $c, @args ) = @_;
    13 
    14   if((@args and $args[0] eq "reset") or
    15    !defined $c->session->{next_question} or
    16    $c->session->{"next_question"} == -1
    17     ) {
    18     $c->session->{"next_question"} = 0;
    19     $c->session->{"score_ok"}      = 0;
    20     $c->session->{"score_nok"}     = 0;
    21     $c->session->{"total"}         = 
    22            $c->model('Questions')->total();
    23     $c->response->redirect($c->uri_for());
    24     $c->detach();
    25   }
    26 
    27   if(my $answer = 
    28           $c->req->param("answer")) {
    29 
    30     if($answer == 
    31        $c->session()->{"correct_answer"}) {
    32 
    33         $c->session()->{"score_ok"}++;
    34     } else {
    35 
    36         $c->session()->{"score_nok"}++;
    37     }
    38   }
    39 
    40   my $next_question = 
    41     $c->session()->{"next_question"} || 0;
    42 
    43   $c->stash->{template} = 'quiz.tt';
    44 
    45   my ($question, @answers) =
    46     $c->model('Questions')->
    47             get_question( $next_question );
    48 
    49   if(defined $question) {
    50     @answers = map { [$_, 'incorrect'] } 
    51                                 @answers;
    52     $answers[0]->[1] = 'correct';
    53 
    54     my $correct_answer;
    55     my $i = 0;
    56 
    57     while (@answers) {
    58       my $pick = splice(@answers, 
    59                         rand @answers, 1);
    60       push @{ $c->stash->{answers} }, 
    61            { text => $pick->[0], 
    62              num => ++$i};
    63 
    64       $c->session()->{"correct_answer"}= $i 
    65           if $pick->[1] eq 'correct';
    66     }
    67     $c->session()->{"next_question"} = 
    68                         $next_question + 1;
    69   } else {
    70       $c->session->{next_question} = -1;
    71   }
    72 
    73   $c->stash->{question} = $question;
    74 
    75   for(qw( total score_ok score_nok 
    76           next_question)) {
    77     $c->stash->{ $_ } = 
    78         $c->session()->{ $_ };
    79   }
    80 }
    81 
    82 1;

YAML als Model

Catalyst arbeitet normalerweise mit Datenbank-basierten Datenmodellen, doch im vorliegenden Fall liegen die Daten in einer YAML-Datei. Ein eigenes Datenmodell zu definieren ist nicht weiter schwierig, der Aufruf

    script/quizshow_create.pl model Questions

legt die Datei lib/QuizShow/Model/Questions.pm an, die wiederum der Entwickler mit dem in Listing Mod-Questions.pm gezeigten Code auffüllt. Die YAML-Datei in Abbildung 3 definiert einen Array von Einträgen für jede der während des Tests dargestellten Fragen. Mit '#' beginnende Kommentarzeilen werden ignoriert. Die Array-Einträge beginnen jeweils an einem Bindestrich ohne weiteren Zusatz und bestehen ihrerseits wiederum aus Arrays, die jeweils vier Elemente enthalten: Den Wortlaut der Frage, gefolgt von der richtigen Antwort und zwei möglichst irreführenden falschen Antworten.

Listing 2: Mod-Questions.pm

    01 ###########################################
    02 package QuizShow::Model::Questions;
    03 # Mike Schilli, 2008 (m@perlmeister.com)
    04 ###########################################
    05 use strict;
    06 use warnings;
    07 use base 'Catalyst::Model';
    08 use YAML qw(LoadFile);
    09 
    10 my $FILE = "/home/mschilli/data/quiz.yml";
    11 
    12 ###########################################
    13 sub total {
    14 ###########################################
    15     my $yml = LoadFile $FILE;
    16     return scalar @$yml;
    17 }
    18 
    19 ###########################################
    20 sub get_question {
    21 ###########################################
    22     my($m, $index) = @_;
    23 
    24     my $yml = LoadFile $FILE;
    25     return undef if $index > $#$yml;
    26     return @{ $yml->[$index] };
    27 }
    28 
    29 1;

In Questions.pm definiert die Variable $FILE den Pfad zur YAML-Datei. Die Methode total() liest die Daten ein und gibt die Anzahl der Fragen zurück, damit die Webapplikation anzeigen kann wieviele Fragen noch ausstehen. total() liefert hierzu den YAML-Array in skalarem Kontext zurück, was in Perl die Arraylänge angibt. Die Methode get_question() weiter unten holt die zu einem Array-Index (0 bis N-1) gehörenden Frage und Antworten hervor und gibt sie als Liste zurück. Falls der Index nicht auf einen gültigen Eintrag zeigt, gibt sie undef zurück. Dies ist für das Online-Quiz das Signal, dass keine Fragen mehr übrig sind und der Endstand angezeigt wird. Will der Controller auf das von ihm abgeschottete Datenmodell zugreifen, schnappt er sich das ihm vorliegende Katalyst-Objekt und ruft $c->model('Questions') auf. Dies gibt ihm eine Instanz des Questions-Datenmodells, dessen Methoden get_question() und total() er anschließend aufrufen kann.

Installations-Dreisprung

Um die fertige Catalyst-Applikation auf einem Produktionsserver zu installieren, führt man einfach den mit CPAN-Modulen üblichen Dreisprung aus:

    cd QuizShow
    perl Makefile.PL
    make install

und Catalyst pflanzt die für die Applikation notwendigen Module, Templates und Skripts in die auf der jeweiligen Plattform definierten Perl-Hierarchie. Statt dem mitgelieferten Perl-Server empfiehlt sich eine mod_perl-Installation, damit Apache2 die Programmlogik auch zügig ausführt:

    PerlModule QuizShow
    <Location />
      SetHandler modperl
      PerlResponseHandler QuizShow
    </Location>

Für Apache 1.3 gibt es ebenfalls eine Konfiguration, die die ausführliche Catalyst-Dokumentation beschreibt. Kommt es nicht auf Geschwindigkeit an, geht auch ein CGI-Skript, das Catalyst als quizshow_cgi.pl gleich mit installiert hat. Stellt man es ins konfigurierte CGI-Verzeichnis des Webservers und ruft den URL http://localhost/cgi/quizshow_cgi.pl/quiz auf, startet das Quiz ebenfalls. Stellt man vom Testserver auf den Webserver um, gilt es zu beachten, dass die Sessions im Verzeichnis /tmp/quizshow abgelegt werden und der Webserver normalerweise unter einem anderen User läuft. Passt man die Nutzerrechte entsprechend an, kann der neue Server den alten Sessionstore übernehmen.

Ein ähnliches Problem stellt sich mit apache2 und mod_perl2: mod_perl2 ist auf Ubuntu mit den Paketen libapache2-mod-perl2 und libcatalyst-engine-apache-perl ruckzuck installiert. Allerings mault der Session-Store Session::Store::FastMmap anschließend, dass er nicht mit Threads zurechtkommt, aber die Alternative Session::Store::File funktioniert prächtig (lib/QuizShow.pm entsprechend anpassen). Allerdings legt Apache2 die entsprechenden Verzeichnisse beim Hochfahren als root an (!) und kann dann nachher nicht mehr darauf schreibend zugreifen, wenn er seine Kinder mit weniger Privilegien startet. Das Kommando sudo chown -R www-data /tmp/quizshow behebt das Problem, indem es dem Session-Store die Eigentumsrechte des Webserver-Nutzers zuweist.

Ausblick

Catalyst bietet viel mehr als die heute vorgestellten Funktionen und taugt nicht nur für kleine sondern ist durchaus für ausgewachsene Großprojekte konzipiert, an denen Teammitglieder in unterschiedlichen Teilbereichen arbeiten. Es bietet ein ausgereiftes Test-Framework, das ebenfalls frei Haus beim Anlegen eines neuen Projektes entsteht. Auch dynamisch aufgefrischte AJAX-Webseiten sind möglich. Mittels eines Erweiterungsmoduls schickt die Webapplikation dann zum Beispiel JSON-Daten auf im Browser wartendes JavaScript, in dem sich dann das mollig-warme Web-2.0-Feeling einstellt, da unnötige Page-Reloads entfallen.

Neben den Online verfügbaren Manualseiten und Tutorials gibt das Buch ``Catalyst'' [4] einen recht guten Überblick, obwohl letzteres weniger zum Nachschlagen geeignet ist, da ihm sowohl ein ordentlicher Index als auch die für ein Referenzwert notwendige Detailtiefe fehlt.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/09/Perl

[2]
``Reicht ihr Latein zum Angeben?'', http://www1.spiegel.de/active/lateintest/fcgi/lateintest.fcgi

[3]
``Candy Bar Identification'', http://food.aol.com/play-with-your-food/candy-bar-id-quiz/?u

[4]
``Catalyst'', Jonathan Rockaway, Packt Publishing, 2007.

[5]
http://www.washingtonpost.com/wp-srv/national/longterm/citizen/citizen.htm

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.