Rezeptbuch
Wer eine größere Website erstellen und verwalten will, kann sich mit Perl-Modulen und Verfahren der OO-Programmierung die Arbeit erleichtern.
- Gerald Richter
Viele dynamische Webseiten basieren nicht mehr auf klassischen CGI-Skripts, sondern auf Vorlagen, bei dem Code-Schnipsel im HTML-Code stehen. Vertreter dieses Vorgehens sind JSP, ASP, PHP oder Mason [1]. Wie Letzteres, interpretiert Embperl [2] in HTML eingestreuten Perl-Code, den es beim Seitenabruf ausführt.
Erstellt man auf diese Weise eine komplexe Website, endet man schnell mit einem Konglomerat aus Code und Layout, in dem die Applikationsfunktionen auf viele einzelne Dateien verteilt sind, die wahrscheinlich viel redundanten Code enthalten. Um größere Projekte erfolgreich erstellen und warten zu können, ist deshalb auf der einen Seite eine Trennung von Layout und Code nötig und auf der anderen eine Aufteilung in Komponenten, um Wiederholungen zu vermeiden.
Auf typischen Webseiten lassen sich meist recht einfach verschiedene Bereiche identifizieren, die sich für eine Zerlegung in Komponenten eignen: Elemente wie Kopf, Fuß und Navigation müssen oft auf jeder Seite vorhanden sein. Der einfachste Ansatz wäre, sie jeweils in eigene Dateien auszulagern und diese mit ‘Include’ in jede Seite einzubinden. Die eigentlichen Seiten enthalten nur noch den für sie spezifischen Teil.
Dieses Vorgehen bringt jedoch Schwierigkeiten mit sich, wenn eine neue Komponente einzufügen oder die Reihenfolge vorhandener Teile zu ändern ist. In diesen Fällen muss man wieder alle Dateien der Site anpacken, und auch beim Erstellen neuer Seiten ist Vorsicht geboten, um keine Komponenten zu vergessen.
Seitenteile wieder verwenden
Embperl umgeht derlei mit seinem Objektmodell, das Embperl::Object implementiert. Zur Illustration dient im Folgenden die Embperl-Website. Ihr vollständiger Quellcode ist in der aktuellen Embperl 2.0-Distribution im Verzeichnis eg/web enthalten (siehe Kasten ‘Quellen und Installation’).
Quellen und Installation
Die Quellen der hier besprochenen Embperl-Website sind Teil der Embperl 2.0-Distribution. Diese ist unter ftp.dev.ecos.de/pub/perl/embperl oder vom CPAN erhältlich. Aktuell ist die Version 2.0b8 - zwar noch eine Beta, jedoch stabil und für den Produktionseinsatz geeignet. Um die Website auf dem eigenen Rechner zum Leben zu erwecken, ist es nötig, Apache 1.3 und mod_perl1.x installiert zu haben.
Sind diese Voraussetzungen erfüllt und ist Embperl übersetzt, kann man mit make start einen Apache starten und die Website unter der URL http://localhost:8531/eg/web/ ansprechen. Die eigentlichen Quellen der Website befinden sich im Unterverzeichnis eg/web.
Zum Anzeigen des Dokumentationsteils ist eine XSLT-Engine erforderlich. Standardmäßig verwendet die Beispiel-Website libxslt (xmlsoft.org). Embperl unterstützt aber gleichfalls XALAN aus dem Apache-Projekt. Dazu muss man die Konfiguration unter test/conf/httpd.conf.src anpassen. Um den Datenbank-basierten Teil testen zu können, muss zusätzlich das Modul DBIx::Recordset (ftp.dev.ecos.de/pub/perl/dbi oder vom CPAN) installiert sowie eine Datenbank angelegt sein; Details dazu in eg/web/README.
Möchte man die am Schluss angesprochene Möglichkeit nutzen, die Ausgabe von Subrequests zu integrieren, sind Apache 2.0 und mod_perl 1.99_02 oder neuer nötig. Im Moment unterstützt Embperl nur die Prefork MPM von Apache 2.0, die Threaded MPM kommt aber in Kürze hinzu.
Alle Seiten sind in vier Bereiche eingeteilt (Abbildung 1): Kopf, Navigation, Inhaltsbereich und Fuß. Statt jedoch jedes Mal diese Elemente einzufügen, gibt es eine Basisvorlage, die Embperl::Object vor der angeforderten Seite aufruft. Im Beispiel heißt diese Basisvorlage base.epl, ihr Name ist in httpd.conf konfigurierbar. Vereinfacht sieht sie so aus:
<html>
<head>
<title>Embperl</title>
</head>
<body bgcolor="#ffffff">
[- Execute ('header.epl') -]
<table width="100%" border="0">
<tr>
<td>[- Execute ('menuleft.epl') -]</td>
<td>[- Execute ('*') -]</td>
</tr>
</table>
[- Execute ('footer.htm') -]
</body>
</html>
Die Seite beschreibt in einfachem HTML das Basislayout und enthält Perlcode-Fragmente, die von [- und -] eingeschlossen sind. In diesem Fall handelt es sich lediglich um Aufrufe der Execute-Funktion, die Embperl zur Verfügung stellt. Verlangt ein Anwender zum Beispiel die Seite index.epl, führt Embperl::Object zuerst base.epl aus. Dieses bindet mit Execute die Dateien header.epl, menuleft.epl und footer.epl ein. Das Besondere ist Execute(‘*’), was die ursprünglich angeforderte Datei lädt, hier also index.epl. Bei diesem Vorgehen braucht man nur noch die Dateien zu erstellen, die den eigentlichen Inhalt der Seiten ausmachen; der Rest kommt bei ihrem Aufruf automatisch hinzu.
Dadurch ist der Inhalt komplett vom allgemeinen Layout getrennt, wodurch es einerseits nicht mehr nötig ist, bei der Erstellung neuer Seiten wiederholende Elemente einzufügen und andererseits eine Änderung des Rahmenlayouts keinerlei Änderungen an den Inhaltsseiten nach sich zieht. Verwendet man für Gestaltung und Aussehen Cascading Style Sheets (in der Basisvorlage definiert oder eingebunden), kann man eine weitgehende Trennung von Inhalt und Layout erzielen.
Was Du ererbt von Deinen Vätern
Nun sind jedoch nicht immer alle Seiten gleich aufgebaut. In der Embperl-Website etwa besteht der Inhaltsbereich auf der Startseite aus zwei Teilen: links Kurzbeschreibung und rechts Newsteil. Bei der Dokumentation jedoch füllt das eigentliche Dokument diesen Bereich ganz aus. Für die Startseite könnte man noch ein Execute(‘news.epl’) hinzufügen, um die zusätzliche Spalte anzuzeigen. Das führt jedoch zu Schwierigkeiten dort, wo kein news.epl vorhanden ist. Statt diese Sonderfälle mit Bedingungen in der Basisvorlage abzuhandeln, bietet es sich an, Execute(‘*’) durch Execute(‘content.epl’) zu ersetzen. content.epl sieht vereinfacht folgendermaßen aus:
<table width="100%" border="0">
<tr>
<td>[- Execute ('*') -]</td>
<td>[- Execute ('news.epl') -]</td>
</tr>
</table>
Auf den ersten Blick scheint dies keine Vorteile zu bringen. Embperl::Object vermag jedoch Dokumente in Unterverzeichnissen zu redefinieren. Da die Dokumentation im Unterverzeichnis pod liegt, erstellt man dort ebenfalls ein content.epl, das lediglich [- Execute (‘*’) -] enthält.
Fordern Anwender nun ein Dokument unterhalb des Verzeichnisses /pod an, beispielsweise pod/doc/index.epl, sucht Embperl::Object zunächst nach der Basisvorlage. Dabei geht es die Verzeichnisse bis zur Wurzel des Webservers durch, also /pod/doc/base.epl, /pod/base.epl, /base.epl. Bei der Suche in DocumentRoot findet es die Datei schließlich. Die Execute-Aufrufe innerhalb des Dokuments lösen nun dieselbe Suche aus: header.epl, footer.epl und menuleft.epl finden sich in DocumentRoot, content.epl jedoch schon unter /pod - und diese Datei wird ausgeführt. Alle Dokumente unterhalb von /pod binden deshalb /pod/content.epl ein, während diejenigen in DocumentRoot das dortige content.epl benutzen. Auf diese Weise ist es möglich, das Basislayout anzupassen, ohne base.epl ändern zu müssen.
Anwendung und Layout trennen
Während nun das Rahmenlayout vom eigentlichen Inhalt getrennt ist, bleibt die Aufgabe, Applikationsfunktionen und Layout zu separieren. Dazu bietet Embperl::Object die Möglichkeit, eine Datei zu definieren, die die eigentliche Anwendung enthält. Für die Embperl-Website heißt sie epwebapp.pl, festgelegt ist der Name in httpd.conf. Wie er nahe legt, handelt es sich hier um reinen Perlcode, in dem Markup nichts zu suchen hat. Nachdem Embperl::Object die Basisvorlage gesucht hat, forscht es auf demselben Pfad nach der Applikationsdatei und lädt sie. Dabei weist es jeder geladenen Datei ein eigenes Package zu und erzeugt eine Hashreferenz, die in dieses Package geblesst ist. Über die Referenz ist der Code innerhalb der Applikationsdatei als Objekt anzusprechen. Da Embperl das package-Statement automatisch einfügt, sollte man es sich in der Datei sparen. Außerdem setzt die Software das @ISA-Array, sodass das Applikationsobjekt von Embperls eigenem Applikationsobjekt abgeleitet ist. Dies ermöglicht den einfachen Zugriff auf die Methoden des übergeordneten Objekts (zum Beispiel Sessionhandling).
Nach dem Laden ruft Embperl die Methode init auf. Diese erhält als ersten Parameter die Objektreferenz für das Applikationsobjekt und als zweiten Embperls Request-Objekt. Vor dem Aufruf von init sind bereits alle Informationen für den aktuellen Request aufbereitet. So sind per GET oder POST übergebene Formulardaten verfügbar, das Sessionhandling ist initialisiert und die Sessiondaten sind ebenso zugänglich wie andere Parameter des Requests. Die meisten dieser Parameter sind über die Methoden des Request-Objekts zugänglich. Die init-Methode des Beispiels zeigt Listing 1.
Listing 1
Diese init-Methode kümmert sich auf der Embperl-Website unter anderem um den Aufbau der Menüs.
sub init {
my $self = shift
my $r = shift ;
my $config = Execute ({object => 'config.pl', syntax => 'Perl'}) ;
$config -> new($r) ;
$r->{config} = $config ;
$r->{menu} = $config->get_menu($r) ;
fill_menu($config, $r->{menu}, $r->{baseuri}, $r->{root}) ;
my $filename = map_file($r) ;
$r->param->filename($filename) ;
return 0 ;
}
Sie lädt zunächst die Datei config.pl, die neben einigen allgemeinen Konfigurationsdaten vor allem die Definition der Navigation enthält, die auf der linken Seite erscheinen soll. Der Parameter object im Execute-Aufruf gibt an, dass die Datei nicht ausgeführt, sondern wie beim Laden der Applikationsdatei ein Objekt erzeugt werden soll. Dieses liefert Execute als Rückgabewert. Anschließend initialisiert new() das neu erzeugte Objekt, und get_menu() liefert die Menüstruktur.
Menüs in beliebigem Format
Wie config.pl die Menüstruktur letztendlich definiert, spielt keine Rolle. Im Fall der Embperl-Website geschieht dies als Perl-Datenstruktur, man könnte aber ebenso gut eine XML-Datei verwenden. Wesentlich ist nur, dass eine definierte Schnittstelle existiert. Objektreferenz und Menüstruktur werden für die weitere Verarbeitung im Request-Objekt abgelegt. Es ist ebenso wie das Applikationsobjekt eine Hashreferenz. Den Hash selbst nutzt Embperl nicht, deshalb kann ihn die Applikation frei belegen. Da das Request-Objekt nur während des Requests existiert, räumt Perl an seinem Ende alle Daten wieder auf. fill_menu() fügt der Menüstruktur weitere für die Anzeige benötigte Daten hinzu. Getreu dem Motto von Trennung von Code und Layout, braucht menuleft.epl die fertig aufbereitete Navigation nur noch anzuzeigen.
Eine weitere Funktion des Applikationsobjekts illustrieren die nächsten beiden Zeilen: map_file() nimmt die angeforderte URI und versucht ein passendes Dokument in der zuvor aufgebauten Dokumenthierarchie zu finden. Hierbei berücksichtigt es zusätzliche Aspekte wie die angeforderte Sprache. Die ermittelte Datei schreibt init() in das Request-Objekt zurück. Da der init-Aufruf vor der Ausführung aller anderen Dateien stattfindet, kann diese Funktion viele Parameter des Requests beeinflussen und verändern.
Dass die Datei mit dem Applikationsobjekt über denselben Pfad gesucht wird wie andere Dateien, kann man zur Definition eines abgeleiteten Applikationsobjekts nutzen. In der Embperl-Website geschieht das im Verzeichnis /db. Die Website stellt verschiedene Informationen zur Verfügung, die sie in einer Datenbank speichert, etwa News, Artikel und Embperl-Sites.
Alle dafür relevanten Seiten liegen unterhalb des Verzeichnisses /db. Es enthält wiederum eine Datei epwebapp.pl, die Embperl lädt, wenn ein Benutzer eine Seite in der /db-Hierarchie anfordert. Sie stellt die nötigen Funktionen für den Datenbankzugriff bereit. Auch hier soll in den eigentlichen Seiten nur die Anzeige stattfinden. Da gleichzeitig Funktionen wie Navigationsstruktur und Datei-Mapping nötig sind, ist das neue Objekt von der Basisklasse abgeleitet:
BEGIN { Execute ({isa => '../epwebapp.pl', syntax => 'Perl'}) ; }
lädt und compiliert das Basisobjekt, gleichzeitig setzt das Statement das @ISA-Array der abgeleiteten Klassen. Listing 2 zeigt die init-Methode für die Datenbank-Applikation.
Listing 2
Die init-Methode für die Datenbank-Applikation wertet in %fdat enthaltene Formulardaten aus.
sub init {
my $self = shift ;
my $r = shift ;
$self->SUPER::init($r) ;
$self->initdb($r) ;
if ($fdat{-add_category}) {
$self -> add_category ($r) ;
$self -> get_category($r) ;
} elsif ($fdat{-add_item}) {
$self -> add_item ($r) ;
$self -> get_category($r) ;
$self -> get_item_lang($r) ;
} elsif ($fdat{-show_item}) {
$self -> get_category($r) ;
$self -> get_item_lang($r) ;
} else {
$self -> get_category($r) ;
$self -> get_item($r) ;
}
return 0 ;
}
Zuerst ruft sie durch SUPER::init die init-Methode der Basisklasse auf; anschließend die eigene Methode initdb, um die Verbindung zur Datenbank aufzubauen und ein DBIx::Database-Objekt zu bekommen. Der Hash %fdat enthält in Embperl die per GET oder POST übertragenen Formulardaten. Anschließend prüfen if-else-Bedingungen, was der Benutzer wünscht und rufen passende Methoden auf, die neue Daten eintragen beziehungsweise Informationen aus der Datenbank lesen. Das Ergebnis legen sie wieder im Request-Objekt ab, sodass es später in der Anzeigeseite verfügbar ist.
Konvertieren: Provider und Rezepte
Nicht nur auf der Embperl-Website liegen Inhalte in unterschiedlichen Formaten vor: Ihre Startseite ist in HTML erstellt, während für die Dokumentation POD (Plain Old Documentation) zum Einsatz kam. Teilweise handelt es sich um (HTML-)Dokumente, in denen aus Sicherheitsgründen Perlfragmente nicht interpretiert werden dürfen. Der syntax-Parameter teilt Embperls Execute-Funktion mit, in welcher Sprache die zu ladende Datei vorliegt. Es gibt eine Reihe vordefinierter Grammatiken (unter anderem SSI, ASP, Perl, Text, RTF, POD), man kann sich jedoch ebenso seine eigene Syntax erstellen.
Schon beim Einlesen der Konfigurationsdatei (Listing 1) informierte syntax=>’Perl’ darüber, dass es um ein reines Perl-Programm ging. Analog sorgt syntax=>’Text’ für uninterpretierte Ausgabe. Spätestens bei POD-Dateien reicht dieser Mechanismus aber nicht mehr aus, da Embperl nicht nur die Syntax verstehen, sondern auch das Markup (hier HTML) erzeugen muss.
Um solche Aufgaben zu lösen, kann man Rezepte (‘Recipes’) definieren. Sie beschreiben die Teilschritte, die ausgeführt werden, um aus den Quellentext das Ergebnis zu erzeugen. Jeden dieser Teilschritte erledigt ein so genannter Provider. Wählt man kein Rezept aus, kommt das Standardrezept zur Anwendung, das die Teilschritte Einlesen, Parsen, Übersetzen, Ausführen und Ausgeben definiert. Neben diesem Standard sind in der Embperl-Distribution verschiedene Rezepte zur Verarbeitung von XML einschließlich XSL-Transformation enthalten. Bieten sie nicht das Nötige, kann man sich sein eigenes Rezept zusammenstellen. Für die POD-Ausgabe greift das Beispiel auf das Rezept EmbperlXSLT zurück und setzt zusätzlich den syntax-Parameter auf ‘POD’, wodurch Embperl die Ausgangsdatei in XML wandelt.
Weiterhin kümmern sich zusätzliche Provider um das Einlesen einer XSL-Datei und das Durchführen der dort vorgegebenen Transformation. Das Ganze folgt dem in Abbildung 2 wiedergegebenen Schema. Rezepte müssen sich also nicht auf lineare Abläufe beschränken, sondern können beliebig komplexe Baumstrukturen definieren. Zwischen- und Endergebnisse dieses Prozesses kann Embperl in einem Cache speichern, um bei häufigeren Abfragen Zeit zu sparen.
Da man nicht bei jeder Datei einzeln konfigurieren möchte, welches Rezept sie benutzen soll, liegt es nahe, dies über Datei-Erweiterungen zu automatisieren. Hierzu kann man wieder das Applikationsobjekt nutzen: Embperl ruft vor der Ausführung einer Datei die Methode get_recipe auf. Überlädt man diese, kann man das passende Rezept ermitteln und zurückgeben. Listing 3 enthält ein gekürztes Beispiel.
Listing 3
Embperls Rezepte erlauben die flexible Wandlung von Ein- in Ausgabeformate - hier geschieht die Auswahl anhand der Namenserweiterung.
sub get_recipe {
my ($class, $r, $recipe) = @_ ;
my $self ;
my $param = $r->component->param ;
my $config = $r->component->config ;
my ($src) = $param->inputfile =~ /^.*\.(.*?)$/ ;
my ($dest) = $r->param->uri =~ /^.*\.(.*?)$/ ;
if ($src eq 'pl') {
$config->syntax('Perl') ;
return Embperl::Recipe::Embperl->get_recipe ($r, $recipe) ;
}
if ($src eq 'pod' || $src eq 'pm') {
$config->escmode(0) ;
if ($dest eq 'pod') {
$config->syntax('Text') ;
return Embperl::Recipe::Embperl->get_recipe ($r, $recipe) ;
}
$config->syntax('POD') ;
if ($dest eq 'xml') {
return Embperl::Recipe::Embperl->get_recipe ($r, $recipe) ;
}
$config->xsltstylesheet('pod.xsl') ;
$r->param->uri =~ /^.*\/(.*)(\..*?)$/ ;
$param->xsltparam({page => $r->thread->form_hash->{page} || 0,
basename => "'$1'", extension => "'$2'"}) ;
return Embperl::Recipe::EmbperlXSLT->get_recipe ($r, $recipe) ;
}
if ($src eq 'epl' || $src eq 'htm') {
$config->syntax('Embperl') ;
return Embperl::Recipe::Embperl->get_recipe ($r, $recipe) ;
}
$config->syntax('Text') ;
return Embperl::Recipe::Embperl->get_recipe ($r, $recipe) ;
}
get_recipe ermittelt zuerst die Namenserweiterung der Quell- und Zieldateien ($src beziehungsweise $dest). Anhand dieser Kombination sucht es das passende Rezept aus. Dadurch ist es möglich, aus derselben Quelle zum Beispiel POD, XML und HTML zu erzeugen - je nach angefordertem Dateityp. Außerdem kann man an dieser Stelle weitere Einstellungen wie Syntax, Escaping, Parameter für die XSLT-Transformation et cetera so verändern, dass sie zum angeforderten Format passen. Es lassen sich nicht nur die vorgefertigten Rezepte und Provider benutzen, sondern auch eigene definieren.
Externe Komponenten einbinden
Im Zusammenspiel mit Apache 2.0 bietet Embperl noch mehr. Anders als seine Vorgänger sendet der neueste Apache seine Ausgaben nicht unbedingt direkt zum Browser, sondern kann sie intern umleiten. Das eröffnet die Möglichkeit, nicht nur von Embperl erzeugte Komponenten einzubinden, sondern alles, was unter Apache 2.0 Ausgaben erzeugen kann. Dazu gibt man die gewünschte URI mit dem Parameter subreq an. Der folgende Aufruf etwa fügt die Ausgabe eine CGI-Skripts ein:
[- Execute ({subreq=>'/cgi-bin/script.cgi'}) -]
Dies wird gerade bei Anwendungen wichtig, die nicht an einem Stück aus dem Boden gestampft sind, sondern bestehende Lösungen integrieren sollen. Durch die Flexibilität des Rezept/Provider-Konzepts sind nicht nur das reine Anzeigen solcher Inhalte, sondern Nachbearbeiten und Filtern möglich. Beispielsweise lässt sich die Ausgabe eines alten CGI-Skripts, dessen Quellcode man nicht besitzt, einbinden und an das aktuelle Layout anpassen.
Ebenso sind unterschiedliche Applikationen, etwa in Java oder PHP geschriebene, unter einer gemeinsamen Oberfläche zusammenzufassen. Ist das Proxy-Module im Apache vorhanden, können die einzubindenden Applikationen sogar auf einem anderem Rechner laufen. Weiterhin ist man damit in der Lage, XML-Daten anzufordern, etwa Nachrichten im RSS-Format, eine XSL-Transformation durchzuführen und das Ergebnis anzuzeigen.
Der Artikel konnte vieles nur streifen, einiges musste unter den Tisch fallen. Jedoch sollte er gezeigt haben, welche Möglichkeiten zur Entwicklung komplexer Websites Embperl bietet. Interessierte finden weitere Informationen auf der Embperl-Website (deutsch) und (englisch).
Gerald Richter
ist Autor von Embperl und für die Firma ecos eletronic communication services GmbH im Bereich Programmierung und Consulting für Inter- und Intranetapplikationen tätig.
Literatur
[1] Peter Dintelmann; Perl-Know-how; Frei gemaurert; Dynamische Webseiten mit Mason erzeugen; iX 2/2002, S. 128
[2] Gerald Richter; Perl-Know-how; In einem Rutsch; Kompakte Datenbankabfragen mit Embperl; iX 9/1999, S. 137
iX-TRACT
- Aktuelle Websites basieren häufig auf HTML-Seiten, die eingebetteten Code enthalten.
- Embperl ist ein Modul, das Perl-Schnipsel in solchen Seiten interpretiert und objektorientiertes Vorgehen ermöglicht.
- Es unterstützt die Trennung von Applikation, Layout und Inhalt.
- Provider und Rezepte erlauben zur Laufzeit die Wandlung beliebiger Formate in HTML; ein eingebauter Cache spart Zeit dabei.
(ck)