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. |
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. |
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.
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.
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.
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.
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;
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.
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.
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.
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.
Michael Schilliarbeitet 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. |