1. PApp - Wie beschreibt man ein Chamäleon?
PApp ist schwer zu beschreiben, da es sich nicht auf ein spezielles Problem spezialisiert, sondern möglichst eine offene Universallösung sein will. Unter anderem ist PApp:
- ein Embedded-Perl-Dialekt. Es gibt verschiedene Formen: Zum einen den "literal programming style", der nicht XML-konform ist, sondern beliebige Daten ausgeben kann und viele Möglichkeiten des Einbettens von Perl in XML. Zum anderen einen Standard-XML-Dialekt, der eine Mischung aus beidem darstellt.
- eine Bibliothek (aus vielen Perl-Modulen), die das Arbeiten in einer CGI-artigen Umgebung erlauben. Die Umgebung hält sich mehr an Apache, ist jedoch unabhängig davon, ob die PApp-Anwendung in einer CGI-Umgebung, unter mod_perl oder (z.B.) unter CGI::SpeedyCGI ausgeführt wird.
- eine Bibliothek, die das Arbeiten mit HTML, XML, SQL und anderen, häufig benutzten Umständlichkeiten vereinfacht.
- eine grosse "State-Maschine", die technisch getrennte Programmteile (z.B. CGI) logisch zu einer Applikation zusammenfasst.
- ein XSLT-Stylesheet-Prozessor. Damit kann man Layout und Inhalt voneinander trennen. Oder auch Ausgabeformat (PDF/HTML/XML etc.) vom Layout. Oder vom Inhalt... Oder der Benutzersprache...
1.1. Warum PApp geschrieben wurde
Grosse und kleine Applikationen für das WWW zu erstellen ist sehr arbeitsaufwendig. Session-Variablen kennt Perl (zum Glück ;) nicht, Session- und Usertracking natürlich auch nicht. Es gibt sehr gute Module, die einem einen Teil der Arbeit abnehmen, aber es ist immer sehr aufwendig und umständlich. Dies führt zu unübersichtlichen Programmen, die noch dazu auf viele Dateien aufgeteilt sind, die nicht unbedingt der inneren Logik des Programmes entsprechen. Auch Sicherheitsfehler (wie das Übergeben von sicherheitsrelevanten Daten über hidden-fields oder ähnliches) passieren schnell, schliesslich muss man sich bei jeder Seite überlegen, wie man seine Daten weiterreicht.
Datenbankabfragen, das Einbauen des Layouts usw. sind sehr umständlich. Internationalisierung existiert praktisch nicht.
PApp vereinfacht alle diese Probleme, indem es sie weitestgehend automatisiert.
2. Grundlegende PApp-Features oder "Nie wieder CGI"
Zunächst möchte ich einige der vielen Features von PApp vorstellen (natürlich die wichtigsten ;). Wem das zu langweilig ist und erstmal eine richtige PApp-Applikation sehen will, sollte zum nächsten Abschnitt gehen, "Eine einfache Anwendung mit PApp".
2.1. Applikationen statt Einzelseitenverwaltungsmonstren
Bei CGI oder anderen Schnittstellen ist die Sicht auf die Anwendung protokollbedingt seitenbasiert - jede "logische" Seite ist eine Datei oder eine Fallunterscheidung innerhalb der Datei - gemeinsame Komponenten müssen in Bibliotheken (oder auch einfachen "Include"-Dateien; das ist im Prinzip das gleiche) abgelegt werden, die auf jeder Seite erneut geladen, konfiguriert und eingebaut werden müssen. Perl-Programme funktionieren jedoch anders, da gibt es keine Zustände, die beim Anklicken wechseln, sondern ein lineares Programm. Dies läßt sich zwar auch mit PApp nicht verwirklichen, aber es tut sein Bestes, diese Einschränkungen aufzuheben, indem es ganze Anwendungen bearbeitet: Ob mehrere Seiten in einer Datei oder eine Seite auf mehrere Dateien verteilt wird ist in PApp egal.
2.2. Development/Maintainance: Aktivposten statt Bremsklötzen
2.2.1. Entwicklungshilfe
Programme müssen erst entwickelt werden. Dabei werden Fehler gemacht. Daran ändert PApp nichts. Da bei PApp sehr viele Komponenten zusammenwirken, können die Fehler entsprechend komplex sein. Eine gute Unterstützung bei der Fehlersuche ist also wichtig.
Dies fängt damit an, dass die Zeilennummern des Quelltextes auch nach Kompilieren von Perl/XML zu reinem Perl oder z.B. in ein XSLT erhalten bleiben. Sollte ein Fehler auftreten, so hat man sofort Zugriff auf die Quelldateien, einen kompletten Backtrace und natürlich die State-Daten. Das Exception-System von PApp ist einfach zu benutzen (die genügt, will man es schöner haben benutzt man fancydie ;) und beliebig erweiterbar.
Im Betrieb will man natürlich keine ausführlichen Fehlermeldungen, womöglich komplett mit Passwort der Datenbank usf... Deshalb protokolliert PApp alle Fehler in eine SQL-Datenbank zusammen mit der State-ID, so dass nur die Fehlerkategorie erscheint (plus ein nettes Textfeld, in dem der Benutzer zusätzliche Informationen eingeben kann). Solange die Seite nur wiederholbare Aktionen enthält, kann der Entwickler den Fehler jederzeit reproduzieren.
2.2.2. Datenbanken
Wie soll man ohne auskommen? Natürlich garnicht. In Perl gibt es ein ganz wunderbares System (DBI!), um mehr oder weniger genormt auf SQL-Datenbanken zugreifen zu können. Jetzt ist "nacktes" DBI recht umständlich, zumindest für mich, für den vier Aufrufe pro SQL-Befehl entschieden zu viel sind. Vor allem, wenn man diese aus effizienzgründen über das Programm verstreuen muss (prepare und execute).
In PApp erledigt man die meisten Aufgaben mit der sql_exec-Funktion (den Rest erledigt man mit sql_fetch/fetchall/exists/insertid). Der folgende Aufruf beispielsweise ersetzt prepare, bind_params, execute und bind_columns:
my $st = sql_exec \my($p, $w), "select purzel, wurzel from table where name like ?", $Ssearch; while ($st->fetch) echo "$p, $w\n";
Der besondere Clou: Die SQL-Statements werden gecached, d.h. im Allgemeinen nur ein einziges mal prepared. Bei Datenbanken, die einiges an Aufwand in diesem Schritt investieren (Stichwort Query-Optimizer) ist das ein grosses Plus. Sogar bei Datenbanken, die dies nicht tun (z.B. MySQL) steigt die Geschwindigkeit merklich. Zudem stehen prepare und das execute nicht mehr weit auseinander (Initialisierung vs. Benutzung!).
Bei Datenbanken, die eine Art "insertid" unterstützen (zum Glück fast alle), kann man diese nach einem INSERT gleich mit abfragen:
my $id = sql_insertid sql_exec "insert into tiere (name) values (?)", "hase";
Den Database-Handle habe ich übrigens nicht vergessen: Wenn man keinen angibt, wird ein Default-Handle benutzt. Geschickterweise wird dieser von PApp selbst gesetzt, so dass man sich im Normalfall noch nicht einmal um die Datenbankverbindung kümmern muss (weil diese auf dem Produktionssystem meistens andere Passwörter benötigt als auf dem Entwicklungssystem, stellt man diese geschickterweise bei der Konfiguration der Applikation ein).
<<<admin.png>>>
2.3. Trennung von Quelltext und Sprache
2.3.1. XML
XML wird in mehreren Teilen von PApp unterstützt bzw. gefordert: Der Standard-Dialekt von PApp ist in XML geschrieben, d.h. um "normale" PApp-Applikationen zu schreiben muss man sich XML bedienen. Die Ausgabe von PApp (meistens Text) ist beliebig, sofern man keine Stylesheets benutzt. Tut man das, muss die Ausgabe natürlich in XML-Syntax sein. Und last-not-least gibt es ein PApp-Modul, mit dem man beliebige XML-Fragmente mit eingebettetem Perl-Quelltext bearbeiten/ausführen/anzeigen kann.
Die Entscheidung, XML so zentral einzusetzen, fiel mir nicht leicht: Meiner Meinung nach ist XML wunderbar dafür geeignet, Daten innerhalb von Programmen zu verarbeiten und vor allem auszutauschen. Toll ist, dass Menschen XML notfalls lesen und auch schreiben können. Ansonsten ist doch die Mächtigkeit von SGML (oder etwas anderem) vorzuziehen.
XML jedoch hat bestechende Vorteile: HTML ist eine XML-Applikation (und HTML ist wichtig ;); es existiert eine breite Unterstützung für XML; es gibt bestehende Standards für Stylesheets, Layout, Metadaten und mehr. Schliesslich ist XML auch noch sehr schnell. Nun ja.
Das hat mich trotzdem nicht davon abgehalten, noch etwas draufzusetzen: Fast alle Dokumente können in einem "Meta-Format" mit eingebettetem Perl-Quelltext geschrieben werden. Die Standard-Syntax dafür bedient sich vier sog. "Modus-Umschalter", die jeweils vom "Verbatim" in den "Perl"-Modus bzw. zurück schalten:
<: schalte von HTML in Perl um <? wie <:, füge jedoch das Ergebnis in die Ausgabe ein :> schalte in den HTML-Modus ?> schalte in den interpolierten HMTL-Modus
Eine HTML-Seite kann man z.B. so schreiben:
<h1>Ein bisschen HTML</h1> <: echo "mit eingebautem perl" :> <: print "so gehts auch, ist aber unendlich wenig langsamer ;)" :> <?"So gehts am schnellsten":> <:for my $text (qw(Noch eine komische Methode)) ?>$text <: :>
Die Modus-Umschalter verhalten sich dabei wie eine Statement-Grenze in Perl (also ;).
2.3.2. I18n
I18n steht kurz für Internationalisierung: Das englische Wort Internationalization hat 18 Buchstaben zwischen dem 'I' und dem 'n', und da das Wort für den Durchschnittsamerikaner viel zu lang und kompliziert ist, hat man diese 18 Buchstaben einfach durch "18" ersetzt.
In Kontext von PApp bedeutet dies, dass beinahe überall Textmeldungen übersetzt werden können (wer das gettext-Paket kennt, mit dem üblicherweise C-Programme internationalisiert werden, wird sich fast sofort Zuhause fühlen). Hier ist ein Beispiel:
<h1>__"Contents"</h1> <:for (1..10) :> <?slink sprintf(__"Chapter %d", $i++), "view_chapter", chapterid => $i:> <::>
Wie man sieht, kann man __"text" sowohl im HTML/XML/wasauchimmer-Quelltext benutzen, als auch auf Perl-Ebene aufrufen. Die __-Funktion erledigt dabei zwei Aufgaben: Zunächst werden damit Textkonstanten zum übersetzen markiert. Zur Laufzeit wird dann in der Übersetzungstabelle nachgesehen und der (evt.) übersetzte String zurückgeliefert.
Wenn man Daten z.B. aus einer Datenbank in eine Variable holt, darf man diese natürlich nicht markieren, da sie ja nur zur Laufzeit einen String enthält, selbst aber keine Textkonstante ist. In solchen Fällen verwendet man gettext:
my $st = sql_exec \my($id, $name), "select id, name from table"; while ($st->fetch) echo "<li>", gettext$name, "</li><br/>";
In welche Sprache jeweils übersetzt wird, entscheidet bei HTTP die Einstellung des Browsers, wobei man zweckmässigerweise ein Menü anbietet, in dem der Benutzer dies überschreiben kann:
<?slink "I want English", SURL_SET_LOCALE("en"):> <?slink "Deutsch willich!", SURL_SET_LOCALE("de"):>
Auch die Spracheinstellung ist eine Preferences-Variable. Im Gegensatz zum gettext-Paket müssen die Quelltexte nicht alle in einer Sprache sein. Bei gettext ist das auch weniger ein Problem, da der Quellcode eines Programms meistens in einer (natürlichen) Sprache geschrieben wurde, in PApp kann man aber Seiten dynamisch einfügen oder Datenbanken übersetzen. Da diese meist nicht vom selben Team geschrieben werden, ist es nützlich, dort unterschiedliche Sprachen verwenden zu können.
<<<poedit1.png>>> <<<poedit2.png>>>
2.3.3. Unicode / Zeichensatzunabhängigkeit
Intern unterstützt PApp genau zwei Datentypen (genau wie Perl selbst): Binärdaten und Text. Binärdaten verwendet man üblicherweise für Bilder, Datei-Downloads und ähnliches. Diese Daten werden von PApp nicht angerührt.
Ganz anders Text: Perl arbeitet intern mit Unicode, das z.Zt. entweder in ISO-8859-1 oder UTF-8 gespeichert wird. Dieser interne Zeichensatz ist völlig unabhängig von der Ausgabe, d.h. der Text wird bei der Ausgabe automatisch in den gewünschten Zeichensatz kodiert (das geschieht wieder Browser/Benutzer/Programmabhängig wobei von den gebräuchlichen Browsern nur Netscape und Lynx die entsprechenden Header liefern). Umgekehrt werden Formulardaten u.ä. automatisch in Unicode gewandelt, d.h. in %P steht Unicode auch wenn der Browser ein Formular in ISO-2022-JP zurückgeschickt hat.
Ein JPEG-Bild kann man z.B. so ausgeben:
content_type "image/jpeg", undef; $gd->jpeg(80);
Während man eine japanische Benutzerin vielleicht mit ISO-2022-JP zufriedenstellen kann:
content_type "text/html", "iso-2022-jp"; echo __"Hoffentlich war der Übersetzer fleissig";
2.4. Trennung von Daten und Layout/Protokoll
Sprache und Quelltext trennt PApp ja schon. Jetzt muss man noch das Layout vom eigentliche Programm bzw. das Protokoll von den eigentlichen Daten trennen.
Mit XSLT-Stylesheets geht das. Und mehr: "Browser XYZ hat ein Problem? Ich fix' es im Stylesheet und vergesse es dann einfach." (z.B. sollte man leere HTML-Tabellenzellen für Netscape lieber mit einem füllen). "Es soll WML (oder CHTML) sein? Ich weiss zwar nicht, wozu das ganze WML-Zeugs sinnvoll sein soll, aber dann mache ich halt' ein Stylesheet dafür." Und eine der wichtigsten Anwendungen: "Wir brauchen eine Version zum drucken. Dafür wandeln wir unser XML-Dokument in XSL" (und dann nach Latex, PDF...).
Das bedeutet, dass man - Planung natürlich vorausgesetzt - einen hohen Grad an Protokollunabhängigkeit erreichen kann, ohne den Quelltext zu sehr mit Layout-Entscheidungen o.ä. belasten zu müssen.
Leider gibt es noch keine Web-Designer, die XSLT statt HTML/CSS liefern, aber (meine) Vision des Webs der Zukunft sieht so aus: Der Server liefert XML zusammen mit einem XSLT (vom Designer), welches XSL erzeugt. Dieses wird dann angezeigt/gedruckt etc.a.. Metadaten (mit dem etwas besseren Nachfolger von RDF) gestatten dann Fragen wie: "Wer hat dieses Dokument geschrieben?", "Wozu dient es?" aber auch: "Wieso ist das Ding so bunt?".
2.5. Trennung von Funktionalität und Ausführungsumgebung
Plattform-Unabhängigkeit: ein toller Begriff. Was bedeutet er? Nicht viel. Bei PApp bedeutet es, dass auf Unix-Abhängigkeiten möglichst verzichtet wurde und sich PApp nicht an einen bestimmten Server (z.B. Apache) bindet. In der Praxis kann man als Platform mod_perl oder CGI verwenden und für Unfälle wie Windoze schere ich mich einen Dreck (d.h. ich habe momentan nicht vor, für proprietäre Schnittstellen Module zu schreiben bzw. habe PApp nur mit den beschriebenen Plattformen getetst). Aber auch andere Umgebungen wie Text-Interfaces sind denkbar: Da PApp der Applikation immer eine gleichbleibende Umgebung anbietet, muss man lediglich ein Schnittstellenmodul schreiben.
2.6. Persistente Variablen für Sessions und User
PApp kümmert sich automatisch um persistente Variablen. So kann man automatisch Variablen erzeugen, die über eine gesamte Sitzung (die mit dem Aufruf der ersten URL beginnt und dann einen Baum bildet) persistent sind. Das geht so einfach, dass man spezielle Hilfsmittel braucht, um nicht an jeder Stelle der Sitzung Zugriff auf beinahe alles zu besitzen.
Persistente Variablen gibt es in verschiedenen Geschmäckern. Es gibt:
- Session-Variablen (sogenannte state keys), die über eine gesamte Sitzung erhalten bleiben. Diese werden im Hash %S gespeichert. Man kann beliebige Werte darin ablegen (solange Storable diese serialisieren kann). Meistens geschieht dies durch Anklicken eines Links, weniger häufig direkt.
- Benutzerabhängige Variablen (die auch über Sitzungsgrenzen hinaus erhalten bleiben und deshalb preferences items, Voreinstellungen, heissen). Auch diese befinden sich in %S, von wo sie automatisch in die Benutzerdatenbank wandern bzw, von dort gelesen werden.
- Lokale Variablen (local keys) zu nur innerhalb einer Seite oder einer Gruppe von Seiten Bedeutung haben. Diese befinden sich ebenfalls in %S und werden automatisch daraus entfernt.
- oder beliebige Kombinationen davon.
Ein Beispiel für eine Sitzungsvariable ist die Information, ob der Benutzer sich schon angemeldet/eingeloggt hat oder ob er bestimmte Zugriffsrechte besitzt. Eine benutzerabhängige Variable ist z.B. die Sprache, die der Benutzer zuletzt ausgewählt hat (global) oder die Anzahl der Tabellenzeilen, die der Benutzer gerne auf einer bestimmten Seite angezeigt haben möchte. Eine lokale Variable ist z.B. ein Datenbank-Objekt, das über mehrere Seiten (z.B. einer Transaktion) gebraucht wird und danach seine Gültigkeit verliert.
Dadurch ergeben sich mehr Möglichkeiten, als man erwarten würde: Unabhängige Komponenten (z.B. Werbebanner oder Foren) sind auf normale Weise nur sehr schwer zu programmieren, da sie immer Hilfe vom "Hauptprogramm" erfordern, wenn es um die Parameterübergabe geht. In PApp benutzt man einfach persistente Variablen.
Ein Beispiel für eine etwas ungewöhnlichere Komponente ist die editform-Bibliothek. Mit ihr kann man HTML-Formulare erstellen, die sich direkt an eine bestimmte Variable binden:
ef_begin; ef_string \$Ssearch; ef_end;
Dieses Beispiel stammt von einer Seite, die Objekte aus einer Datenbank anzeigt und sich dabei auf solche beschränkt, die ein Suchwort enthalten. Das Formular bindet die State-Variable search an ein HTML-Textfeld. Bei der Ausgabe wird der aktuelle Wert von $Ssearch benutzt. Ändert der Benutzer den Text und schickt das Formular ab wird die Seite neu aufgebaut - mit einem anderen Wert in $Ssearch. Da editform beliebige Perl-Referenzen akzeptiert und man in PApp auch Referenzen auf Dateien, SQL-Spalten etc... erzeugen kann, werden viele Formulare zum Kinderspiel.
Zusätzlich zu den Sitzungsvariablen gibt es noch sogenannte Argumente, die nur eine Seite lang "persistent" sind, d.h. bei einem Link auf eine andere Seite übergeben werden:
echo slink "Klicken Sie hier", -argument1 => "wert1", var2 => "wert2";
Wenn man diesem Link folgt, steht der "wert1" im Hash %A bzw. "wert2" im Hash %S:
printf "Das Argument argument1 ist %s, die State-Variable var2 ist %s", $Aargument1, $Svar2;
Werte, die von "Aussen" (z.B. GET-Request in CGI) kommen, stehen, um die Verwirrung komplett zu machen, in %P. Als Faustregel gilt: was in %S und %A steht ist sicher (bzw. man hat es selbst dorthin gepackt), was in %P steht ist unsicher und muss erst gefiltert/geprüft werden.
2.7. Sicherheit
In vielen CGI-Programmen (aber nicht nur dort) wird der Fehler begangen, sensitive Daten in hidden-Feldern in einem Formular zu "verstecken" oder sie in die URL zu kodieren um die Parameterübergabe zu realisieren. Dies ist in PApp nicht nötig, da der persistente 'State' in einer Datenbank gespeichert wird und nur der (mit 256 Bit verschlüsselte) Zeiger darauf zum Client übertragen wird. Dadurch werden sensitive Daten gar nicht erst zum Client übertragen, was natürlich auch Bandbreite spart. Sollte der Schlüssel einmal bekannt werden kann man zwar auf andere Sessions zugreifen, die Daten jedoch immer noch nicht verändern (dieser Fall tritt beispielsweise auch auf, wenn ein kaputtes Proxy zwischen Server und Client sitzt).
Eine typische PApp-URL sieht übrigens so aus (Applikation "kis", Modul "abteilungswahl"):
/kis/abteilungswahl/NIlRSJNDIfP3AHfVjxLF5F
Da eine "Seite" in PApp aus einem Modulbaum besteht, geht es auch komplizierter:
/exec/admin/+admin=poedit+poedit=view--?papp=eTDgL.I-lj9O9lTO3wznTk
Zusammen mit einem sicheren Transportprotokoll (z.B. SSL, wobei die meisten Browser nur ungenügend oder garnicht gegenüber Attacken schützen, nur so am Rande ;) schützt man sich damit gegen alle Seiten.
Benutzerauthentifizierung über Zertifikate ist auch im Einsatz: Mit dem ssl-Modul von Stefan Traby gibt es seit neuestem eine komplette, transparente SSL-Integration in das PApp User-Management: Nach einem "<:ssl_user_needed:>" hat man beispielsweise eine garantierte SSL-Session mit User-Zertifikat und einem eindeutigen PApp-user. Das CA-Management-Tool ist in Vorbereitung.
<<<janman.png>>>
2.8. Session/User-tracking
Persistente Variablen erfordern Session-Tracking. Benutzerabhängige Einstellungen (Preferences) darüber hinaus auch User-Tracking. Ersteres geschieht mit einer Session-ID, die (auf verschiedene Arten) in die URL-kodiert wird und die alle notwendigen Daten enthält, um die gewünschte Seite komplett zu erzeugen. Darüber hinaus wird (optional) ein Cookie benutzt, das aber z.Zt. nur zum identifizieren des Benutzers dient, da ich Session-Definitionen über Cookie als Unsinn erachte. Das Cookie wird auch nur einmal am Tag gesetzt, so dass man auch mit der Browser-Einstellung "vor Cookies warnen" arbeiten kann.
Eine Anwendung wird darüber informiert, ob eine neue Session begonnen wurde oder ob sich ein bisher unbekannter Benutzer angemeldet hat, so dass man Benutzereinstellungen o.ä. initialisieren kann. Dies nützt PApp selbst, um sich z.B. die gewünschte Sprache des Benutzers zu merken oder um das User-Cookie nur einmal täglich zu setzen.
Eine "Sitzung" ist übrigens ein Baum (nicht nur in PApp), der mit dem ersten "Hit" als Wurzel beginnt. Dadurch ist es unter anderem möglich, die Anzahl der "Reloads" einer Seite zu bestimmen. Dies ist nützlich, um potentiell gefährliche Aktionen (z.B. Löschen von Datenbanken, abschicken von E-mails) nur einmal auszuführen.
2.9. Benutzerverwaltung
Da PApp Benutzer (mehr oder weniger) eindeutig identifizieren muss, implementiert es intern eine eigene Benutzerverwaltung inklusive einer Unix-artigen Rechtevergabe (es gibt allerdings keinen Super-User im Unix-Sinne). In vielen Anwendungen kann man diese gleich mitbenutzen, da eine eindeutige Benutzer-ID automatisch vergeben wird.
Sie sind wahrscheinlich Benutzer Nummer <?$userid:><br/> #if auth_p Und ausserdem haben sie sich authentifiziert.<br/> # if access_p "admin" Oh, und "admin"-Rechte besitzen Sie auch noch! Meine Güte!!<br/> # endif #endif
2.10. Geschwindigkeit und Skalierbarkeit
Geschwindigkeit war bei der Implementierung von PApp das zweitwichtigste Ziel (Korrektheit ist das wichtigste ;). Als Marke dient mir ein Pentium-II 266Mhz-Rechner, auf dem auch komplexe Seiten mit mindestens 15 Hits/Sekunde dargestellt werden, bzw. der Vergleich mit einem ähnlichen Apache::Registry-Skript.
Das State-Management von PApp verschlingt pro Seite 1-3 Datenbankzugriffe, die die Zeit bei weitem dominieren (andere Features wie I18n sind im Prinzip kostenlos). Da komplexe Seiten im Allgemeinen wesentlich mehr und kompliziertere Zugriffe enthalten bzw. man ja irgendwie seine Daten speichern muss, ist dies in der Praxis selten eine Einschränkung (Ausnahmen gab und gibt es immer). Andere PApp-Features (z.B. im SQL-Bereich) bringen häufig sogar eine Steigerung der Geschwindigkeit.
Da ich bisher keine Seite fabrizieren konnte, die die 15 Zugriffe/s-Marke unterschritten hätte, glaube ich noch etwas Spielraum für noch mehr Features zu haben. Wer sich übrigens fragt, woher diese magische Grenze von 15 Hits/s kommen: 15 Hits ist die Marke, die einen wirklich grossen Server von den 99.99% der restlichen Welt unterscheidet. Hat man auf seinem Server 15 oder mehr Zugriffe pro Sekunde kann man sich meistens auch eine schnellere Datenbank oder mehrere Rechner leisten: PApp skaliert problemlos auf mehrere Maschinen oder SMP-Rechner.
2.11. Ideen, Ideen: z.B. "tied forms"
Ich habe es schon angesprochen: Persistenz von fast allem, zusammen mit den Möglichkeiten von Perl können einen schon auf Ideen bringen, z.B. auf eine Bibliothek (editform), die HTML-Formularfelder an Perl-Refrenzen bindet.
Nun ist es an der Zeit, einmal eine Referenz auf eine SQL-Tabelle zu erzeugen:
<: my $row = new PApp::DataRef 'DB_row', table => "user", where => [id => $userid], delay => 1, autocommit => 1; # pre-set name $row->name ||= "<username>"; ef_begin; :><br>__"ID:" <?ef_string \$row->id , 5:><: :><br>__"Name:" <?ef_string \$row->name, 20:><: ef_submit __"Update"; ef_end; :>
Die Referenz auf die Zeile steht in $row und verhält sich wie eine Referenz auf einen herkömmlichen Perl-Hash. Das autocommit sorgt dafür, dass die geänderten Daten automatisch zurückgeschrieben werden sobald das $row-Objekt gelöscht wird (ausser Scope geht, DESTROYed wird). Sie ist lokal (my!) gespeichert, da aber die ef_-Funktionen die Referenz persistent speichern, wird das Objekt erst auf der nächsten Seite (also nachdem die Ergebnisse hineingeschrieben wurden!) zerstört, bzw. die Daten geschrieben. Etwas kompliziert, aber zum benutzen muss man die Details ja auch nicht kennen.
Nun macht das Beispiel keine Fehlerüberprüfung, meistens das schwierigste. Mit PApp kein Problem. Zuerst schalten wir das autocommit ab, damit die Daten nicht "aus Versehen" geschrieben werden. Ausserdem übergeben wir die Referenz als Argument an die "nächste" Seite.
<: my $row = $Arow || new PApp::DataRef 'DB_row', table => "user", where => [id => $userid], delay => 1, autocommit => 0; ef_begin -row => $row; # Daten als Argument übergeben
Nun brauchen wir ein Flag, das uns sagt, ob der Datensatz fehlerhaft ist und schon können wir loslegen:
my $err = 0; # Daten fehlerhaft? :><br/>__"ID:" <?ef_string \$row->id , 5:><: #if $row->id !~ /^(\d+)$/ <error>__"The ID must be an integer!"</error> <:$err++:> #endif :><br/>__"Name:" <?ef_string \$row->name, 20:><: #if 2 > length $row->name <error>__"The Name must contain at least two characters!"</error> <:$err++:> #endif ef_submit __"Update"; ef_end;
Die Daten schreibt man dann bei Bedarf in die Datenbank:
#if $row->dirty # if $err <error>__"The entered Data is invalid, please surrender or die (or correct it ;)</error> # else <:$row->flush:> __"The record has been updated." # endif #endif :>
2.12. "Web-Widgets"
Eine relativ neue "Neuerung" in PApp ist die Einführung von Web-Widgets. Wie so vieles sind das keine speziellen Objekte, sondern eine Art der Programmierung, die zwar schon immer möglich war, aber an die man nicht sofort denkt.
Statt einer ganzen Applikation, die "ganze Seiten" (z.B. ganze HTML-Seiten) ausgibt, schreibt man eine Applikation, die nur noch Teilseiten ausgibt, die man in größere Applikationenn einbaut. Dies ist nicht das gleiche wie ein "include": Der Namensraum (globale Variablen, state-keys, also Session-Variablen und Preferences) einer solchen eingebetteten Applikation ist getrennt von der einbettenden Anwendung und bildet eine Art "Unternamensraum".
Derlei eingebettete Anwendungen sind vollkommen autark, haben ihren eigenen Zustand ("aktuelle Seite") und können ihre eigenen Formulare, Links etc. erzeugen die mit anderen Applikationen nicht kollidieren. In einer Anwendung verwende ich dasselbe Forum-Element, um "Web Chat", "Kleinanzeigen" und eine "News!"-Seite zu implementieren - jedesmal eine leicht andere Konfiguration aber derselbe Code.
Da PApp-Applikationen im Prinzip nur grosse Statemaschinen sind (das ist eine Einschränkung der verwendeten Protokolle, d.h. bis Perl effektive und schnelle continuations bekommt ;), kann man sie auch einfach in andere einbauen. Sie können sich gegenseitig beeinflussen, treten sich aber nicht auf die Füsse.
Also im Prinzip genauso wie ein "Widget" in X11 oder gtk+.
2.13. Logging/Protokollierung
PApp protokolliert wesentlich mehr, als man benötigt, legal wäre, und speicherbar wäre. Da PApp jederzeit in der Lage sein muss, eine ältere Seite zu regenerieren ("Back & Reload"), speichert es pro Seite die persistenten Variablen (ca. 300-900 Byte, je nach Applikation, das ist, wie gesagt, auch eine tolle Sache zum debuggen).
Aber irgendwann müssen diese Daten wieder weg bzw. statistische Daten her - nichts ist interessanter, als Zugriffsmuster, Voreinstellungen oder ähnliches, an dem man ablesen kann, welche Dinge beliebt sind und welche nicht (klar, man kann auch ganz andere Sachen auswerten, aber das ist nicht mein Problem).
Beim Aufräumen spielt PApp die einzelnen Zugriffe noch einmal durch. Statt jedoch Seiten zu erzeugen gibt PApp den einzelnen Applikationen die Möglichkeit, statistische Daten zu sammeln. Etwas undokumentierter (wenn es da Abstufungen gibt) ist die Möglichkeit, globale Daten zu sammeln, aber es geht...
3. Eine einfache Anwendung mit PApp
Jetzt kommen wir zur eigentlichen Frage: Wie sieht so ein PApp-Programm aus? Nun, meistens so:
3.1. Das Hauptprogramm
<package name="demo"> <domain lang="en"> <database dsn="DBI:mysql:demodb"/> <translate fields="project.name place.name" lang="de"/> <translate fields="project.description" lang="*" style="auto"/> <import src="macro/util"/> <include src="demo/somepages"/> </domain> </package>
Hmm... das ist also erstmal XML mit furchtbar vielen neuen Elementen. Normalerweise kopiert man sich einfach ein anderes Programm und schreibt nicht alles neu. Gut: zuersteinmal das package-Element: damit wird - genau wie in Perl - ein neuer Namensraum erzeugt, der sowohl normale Package-Variablen als auch State-Variablen enthält. Nicht jedes Programm muss einen eigenen Namensraum öffnen.
Das nächste Element, domain, aktiviert die Übersetzungen: Alle markierten Texte werden unter einem Namen, der domain gebündelt. Beispielsweise befinden sich alle Texte des PApp-Systems selbst in der "papp"-Domain. Da im Beispiel kein Domainname angegeben wird, nimmt PApp den Namen des umschliessenden package-Elements, d.h. wir definieren hier eine Übersetzungsdomain "demo".
Das nächste Element deklariert die Standarddatenbank: Wird die Datenbankhandle bei SQL-Abfragen weggelassen, wird diese Datenbank benutzt. Normalerweise deklariert man Datenbanken aber nicht im Quellcode, sondern beim Einrichten der Anwendung im Konfigurationsmenü.
Zu den nächsten beiden Elementen muss ich etwas über das Programm erklären, das ich hier entwickeln möchte: Das Demo-Programm sollte kurz sein und die wichtigsten Features von PApp zeigen. Es wird aus drei (Web-) Seiten bestehen, eine Art Hauptseite mit einem Menü und zwei Unterseiten, auf denen man ein Projekt auswählen kann (ein Projekt ist ein einfacher Datensatz in der Datenbank, der den Projektnamen, den Ort und die Beschreibung enthält). Auf der dritten Seite kann man die einzelnen Felder eines Projektes ansehen und ändern.
Als besonders überflüssiger Schnickschnack sollen die Projektdaten übersetzbar sein, d.h. in der Sprache des Benutzers angezeigt werden. Das ist nicht sehr wirklichkeitsnah, gibt dafür aber ein sehr simples Beispiel ab.
Da die Projektdaten in der Datenbank gespeichert sind, kann man sie nicht einfach mit __"xxx" markieren, sondern braucht ein translate-Element. Wo PApp die Übersetzungen herbekommt, ist relativ egal, man muss PApp nur sagen, wie es an die Texte herankommt und in welcher Sprache sie sind.
Das erste translate-Element sagt, dass die Spalten name in der Tabelle <project> und die Spalte name in der Tabelle place in Deutsch sind und dementsprechend in andere Sprachen zu übersetzen sind.
Das zweite translate-Element ist schon komplizierter: Nicht alle Projektbeschreibungen sind in derselben Sprache (behaupte ich einfach mal), weshalb als Sprache * angegeben wird. Das bedeutet lediglich, dass alle Übersetzer diese Texte übersetzen müssen. Wenn der Englisch-Deutsch-Übersetzer also auf einen Deutschen Text stößt, muss er ihn einfach überspringen. Wäre die Spalte als "de" (oder "deu") markiert, würde er sie garnicht erst zu Gesicht bekommen.
Da Beschreibungen ausserdem sehr lang sein können, erhält man mit style="auto" die Möglichkeit, die __"xxx"-Syntax auch in den Beschreibungen zu verwenden. Dies kann man auch für einfache Textbausteine mißbrauchen...
Das darauffolgende import-Element ist wieder etwas einfacher: es entspricht mehr oder weniger der use-Anweisung in Perl (in genau die wird es auch übersetzt), bezieht sich aber auf PApp-Dateien. Damit kann man Funktionen aus anderen (PApp-) Namensräumen importieren. Im vorliegenden Fall importiere ich einfach mal das macro/util-Paket, das sich immer wieder grosser Beliebtheit erfreut.
Das letzte Element tut zur Abwechslung genau das, was es sagt: es fügt (logisch gesehen) eine andere Datei an dieser Stelle ein. Die Entscheidung, die folgenden Seiten in eine andere Datei auszulagern, lohnt sich normalerweise nur für grössere Programme, aber so bleibt das Beispiel klein.
Bis jetzt tut sich noch nichts. Ändern wir das:
3.2. Das erste PApp-Modul
Einzelne Zustände (z.B. Webseiten) werden bei PApp "Module" genannt, wohl einzig um die Anwender zu verwirren. Diese Module werden meist in andere Elemente (domain, package, style etc..) verpackt, die auf die Sete auf verschiedenste Weise einwirken.
Ausführbaren Code kann man grundsätzlich in zwei Formen angeben: Normaler Perl-Code und Perl-Code gemischt mit Text (also wie bei anderen embedded-Dialekten):
<module name=""><phtml><![CDATA[ <html> <title> __"PApp - demo", <?localtime:> </title> <?slink "Deutsch", "/papp_locale" => "de":> <?slink "English", SURL_SET_LOCALE('en'):> <p><?slink __"To the editor", "editor":> <p><?slink __"Edit project 1", "editor", projectid => 1:> </html> ]]></phtml></module>
Zuerst zum module-Element: Jedes Modul besitzt einen eindeutigen Namen. Das Standardmodul (das angezeigt wird, wenn nichts spezielles ausgewählt ist, also sozusagen die Startseite) ist das Modul mit dem leeren Namen: "".
Das Ergebnis eines Moduls sind die Ausgaben, die darin gemacht werden (z.B. mit printf oder dem PApp-echo). Die Ausgabe des obersten Moduls (im Baum) wird an den Browser geschickt. Für Ausgabe braucht man Perl und das habe ich in ein phtml-Element gepackt (für verbatimen Perl-code würde man ein perl- oder xperl-Element verwenden, für Perl gemischt mit XML gibt es noch pxml).
Innerhalb des phtml-Elementes darf nur eine Zeichenkette stehen, die ausgegeben wird. Da wir innerhalb der Zeichenkette Perl einbetten wollen und die Modus-Umschalter kein gültiges XML sind, muss der Inhalt geschützt werden, in diesem Fall mit einem CDATA (die Verwendung von Abkürzungen in VI o.ä. empfiehlt sich ;).
Da dies das oberste Modul ist und wir (noch) kein Stylesheet verwenden, müssen wir eine ganz HTML-Seite ausgeben. Als Titel nehmen wir den Text PApp-demo, der übersetzt werden muss sowie, weil es so schön ist, die aktuelle Uhrzeit. Dies geschieht mit einem <?: Der Ausdruck (hier localtime) wird in einem skalaren Kontext ausgewertet und das Ergebnis ausgegeben.
Die nächste Code-Zeile ist interessanter:
<?slink "Deutsch", "/papp_locale" => "de":>
slink ist PApps Art, eine Hypertext-Referenz (A-Element) zu erzeugen. slink erwartet als erstes Argument den Inhalt des Verweises (der Text, den der Benutzer "anklicken" muss) und daraufhin die sogenannten surl-Argumente, die bei vielen PApp-Funktionen angegeben werden können. surl-Argumente sind im wesentlich "Name => Wert"-Paare, die dem Zielmodul als Argumente übergeben werden können. Im Beispiel ist das die Variable /papp_locale (der Slash am Anfang markiert eine globale Variable), die auf den Wert "de" gesetzt wird. Das Ergebnis ist, dass Textmeldungen nun auf Deutsch übersetzt werden.
<?slink "English", SURL_SET_LOCALE('en'):>
Neben Argumenten kann man auch bestimmte "Cookies" in die surl-Argumente einbauen. SURL_SET_LOCALE z.B. setzt die Sprache auf den folgenden Wert und speichert ausserdem die Voreinstellungen ab, d.h. die Sprachwahl ist nun permanent. Neben SURL_SET_LOCALE gibt es noch eine ganze Reihe weitere "Cookies" wie z.B. SURL_EXEC, mit dem man bei Auswahl des Links eine Unterroutine aufrufen lassen kann oder SURL_STYLE_GET, mit dem man den Stil der URL einmalig überschreiben kann. Die nächste Zeile bringt eine weitere Erweiterung:
<p><?slink __"To the editor", "editor":>
Bisher wurde kein Ziel für den Link angegeben. In solchen Fällen nimmt PApp die aktuelle Seite (das aktuelle Modul) als Ziel, d.h. sie wird einfach neu geladen (bei Sprachänderungen etc. ja gewünscht). Möchte man ein anderes Modul ansteuern, gibt man einfach den Namen des Moduls an, z.B. "editor". Ein "Klick" auf den Link (oder das Laden der URL, wie auch immer das geschieht) ruft dann das editor-Modul auf.
Das "Ziel" muss auch nicht immer ein Modulname sein, es geht auch komplizierter: "admin,user/edit,group/" z.B. verzweigt auf das admin-Modul und gleichzeitig in einem eingebetteten Widget user auf das Modul edit, während es im Widget group auf das Hauptmodul (das mit dem leeren String als Namen) schaltet. Aber das braucht man wirklich nur in grossen Projekten ;).
Ziel und Argumente kann man natürlich kombinieren:
<p><?slink __"Edit project 1", "editor", projectid => 1:>
Hier wird dieselbe Seite (editor) angesteuert, dieses mal wird jedoch ein Argument übergeben. Beim Aufruf wird dieses Argument in den State geschrieben, steht dem editor-Modul also als $Sprojectid zur Verfügung. Manchmal möchte man einfach nur ein Argument übergeben, dass nicht im State endet, d.h. nicht persistent ist. In diesem Fall kann man vor den Namen ein '-' stellen, der Wert landet dann nicht in %S sondern in %A und wird nach dem Aufruf weggeworfen. Eine dritte Möglichkeit ist es, eine Referenz auf einen Skalar anzugeben (der natürlich persistent sein muss, sonst existiert er beim Aufruf nicht mehr).
Die Werte dürfen beliebige Perl-Referenzen sein, solange sie serialisierbar sind. In PApp gehören Code-Referenzen und (PApp-) Datenbankhandles übrigens zu den serialisierbaren Datentypen, wenn man etwas Vorsicht walten lässt.
Der erste Link auf editor soll, da er kein Projekt auswählt, eine Liste aller Projekte zeigen während der zweite ein spezielles Projekt (das hoffentlich existiert) anzeigen soll. Damit wäre die erste Seite erklärt, schreiten wir zum editor-Modul:
3.3. Das editor-Modul
Ich gebe zu, ich habe etwas gemogelt. Natürlich macht es normalerweise mehr Sinn, zwei getrennte Seiten (z.B. project_list und project_edit) für das Listen und Edieren zu benutzen. Aber dann hätte ich keinen Grund, ein weiteres Stückchen "syntactic sugar" zu zeigen, Präprozessorkommandos:
<module name="editor"> <state keys="projectid" local="yes"/> <phtml><![CDATA[ <:header:> #if defined $Sprojectid ... edit the specific project #else ... show a list of projects #endif <:footer:> ]]></phtml></module>
Das module-Element kennen wir ja schon. Diesmal deklariert es das editor-Modul. Das nächste Element state ist schon interessanter: hier markiert es bestimmte State-Keys (hier: projectid) als local, d.h. lokal zu allen Seiten, die diese Variable so markieren. Da das Hauptmodul die projectid nicht als lokal markiert, wird sie beim "Klick" auf das Hauptmodul automatisch gelöscht. Hätte man stattdessen geschrieben
<state keys="projectid bgcolour" preferences="yes"/>
hätte man die beiden State-Keys als Voreinstellungswerte markiert, d.h. beim nächsten Aufruf würde der Benutzer das gleiche Projekt sehen (und eventuell die gleiche Hintergrundfarbe, je nachdem, was bgcolour bedeutet).
Die beiden "Tags", <:header:> und <:footer:>, sind eigentlich nur getarnte Funktionsaufrufe: In den Tagen vor XSLT habe ich meistens auf diese Weise ein Standard-Layout erstellt. header könnte man z.B. so definieren:
<macro name="header"><phtml><![CDATA[ <html> <head><title>__"Hallole"</title></head> <body> ]]></phtml></macro>
macro definiert eine ganz normale Perl-Funktion, sie kann Argumente annehmen und Werte zurückliefern. Der einzige Unterschied ist, dass man Perl-Funktionen auf diese Weise in "phtml"-Syntax schreiben kann. Um einzelne Seiten gegen unberechtigen Zugriff zu schützen (und um noch mehr abzuschweifen) habe ich früher folgendes gemacht:
<macro name="page(&)" args="$body"><phtml><![CDATA[ <html> <body> #if access_p "project_editor" <:&$body:> <!-- eigentliche seite anzeigen --> #else __"You need to login yourself first"<p> <:loginbox:> <!-- loginbox anzeigen --> #endif </body> </html> ]]></phtml></macro> <module name="secure_page"><phtml><![CDATA[ <:page :> <h1>__"Hallo"</h1> <::> ]]></phtml></module>
Gut, zurück zum Thema: Die Aufgabe ist klar: ist eine projectid gegeben, soll das entsprechende Projekt ediert werden, sonst soll eine Liste von Projekten zur Auswahl angeboten werden.
Das könnte man zwar mit einem if lösen, es geht aber auch anders:
#if <perl-ausdruck> ... #elif <perl-ausdruck> ... #else ... #endif
Das ist fast so schön wie C... *hüstel*. Jedenfalls wird es in ein hundsnormales Perl-if/elsif/else/endif umgesetzt. Die beiden Teile "edit the specific project" und "show a list of projects" werden sofort gefüllt:
3.3.1. "show a list of projects"
Zuerst die Liste der Projekte. Ah, wir benötigen SQL. Nun, das ist einfach, wir brauchen nur "jede Menge" HTML auf das Problem zu werfen:
<table><tr><th>__"Project"<th>__"Budget" <: my $st = sql_exec \my($id, $name, $budget), "select id, name, budget from project"; while ($st->fetch) :><tr><td> <?slink gettext$name, projectid => $id?> <td>$budget <: :> </table>
Dies erzeugt eine einfache HTML-Tabelle mit den beiden Spaltenüberschriften "Project" und "Budget", natürlich übersetzbar. Der Aufruf von sql_exec tut drei Dinge:
- prepare, falls notwendig (preparete Statements werden gecached).
bind_columns, auf die drei Referenzen \$id, \$name und \$budget.
execute, um die Abfrage zu starten.
Nun müssen wir nur noch in einer Schleife über die Zeilen iterieren (mit dem alten DBI-Bekannten fetch) und TR/TD-Zeilen ausgeben. In der Schleife kann man sehr schön sehen, wie man zwischen Perl und HTML umschalten kann. Keine Angst, daran gewöhnt man sich sehr schnell, vor allem mit Syntax-Hilighting (ein weiterer Grund, auf vim umzusteigen).
Das einzig Komplizierte ist der Aufruf von slink, der einen A HREF-Link auf das editor-Modul (also auf die aktuelle Seite) erzeugt und diesem gleich noch die aktuelle Projekt-ID mitgibt. gettext ist der Runtime-Teil der __-Funktion, d.h. die Funktion, die von __ zur Laufzeit aufgerufen wird. Sie übersetzt das Argument, markiert es aber nicht.
Das Ergebnis ist ein Link, der beim Aufruf die Seite neu lädt, nur mit dem Unterschied, dass diesmal eine Projekt-ID verfügbar ist also nicht mehr die Liste angezeigt wird.
3.3.2. "edit the specific project"
Der Rest ist nun relativ einfach: Mit DataRef eine Referenz erzeugen, mit editform ein Formular, der Rest geht von alleine:
<:ef_begin:> <:my $row = new PApp::DataRef 'DB_row', table => "project", where => [id => $Sprojectid]:> <p>__"Name": <:ef_string \$row->name, 40:> <p>__"Place:" <:ef_relation \$row->place, "id, name from place order by 2", 0 => __"unknown":> <p>__"Budget": <:ef_string \$row->budget, 8:> <p>__"Description": <:ef_text \$row->description, 60, 10:> <:ef_constant \$row->user, $userid:> <:ef_submit __"Update":> <:ef_end:>
new PApp::DataRef erzeugt eine Zeilenreferenz auf die Zeile mit dem gewünschten Projekt, ef_begin und ef_end umschliessen ein Formular, ef_string erzeugt ein Textfeld in der gewünschten Breite während ef_text ein textarea-Element erzeugt. ef_submit sollte selbsterklärend sein.
Habe ich was vergessen? Oh ja, der Ort ist ja in einer separaten Tabelle, in project wird ja nur die ID des Ortes gespeichert. Für solche Relationen gibt es bei editform das ef_relation-"Element". Man übergibt einen SQL-Ausdruck, der zwei Spalten (ID und Name) liefert und optional ein paar weitere Paare (z.B. kann man auch "unbekannt" angeben). Als Ergebnis erhält man ein select-Element in HTML.
Als letztes wird ein Formularfeld noch mit einer Konstanten (die der Benutzer nicht ändern kann) gefüllt, in diesem Fall wird das Feld user auf die aktuelle userid gesetzt, so weiss man immer, wer den Datensatz zuletzt verändert hat.
Wer Formulare in anderen Sprachen (WML...) benötigt, kann sich seien eigenen Elemente basteln: editform ist es grundsätzlich egal, wie die Syntax ist, solange das Schnittstellenmodul von PApp die Daten dekodieren kann.
3.4. Aufgemotzt hält besser
Bis jetzt war alles noch langweilige Grundlagen. Vor allem ist Standard-HTML doch soo langweilig: die Tabellen sind nicht farbig hinterlegt, um das Lesen zu erleichtern. Und die header- und footer-Methode ist auch nicht gerade ein flexibles Layout-Werkzeug.
Deshalb: XSLT muss her ("eXtensible StyLesheet Transformations"). Und weil ich so anspruchsvoll bin, gleich zwei Stylesheets: eins ohne aufwendige Grafik zum benutzen und eins mit vielen farbigen Elementen zum verkaufen ("bunt haben wollen").
Zuerst müssen wir das Stylesheet laden:
<perl><![CDATA[ $stylesheet[0] = $papp->load_stylesheet("demo/demo1"); $stylesheet[1] = $papp->load_stylesheet("demo/demo2"); ]]></perl>
Naja, zuerst müsste man es schreiben oder besser klauen. Ausserdem muss man es nicht zuerst laden, aber darüber gehe ich einfach mal hinweg...
Das perl-Element ist übrigens nicht in einem Modul: Perl-Code, der nicht in ein Modul gesteckt wird, wird beim Laden der Applikation ausgeführt, d.h. ganz ähnlich wie ein Perl-Modul. Zu diesem Zeitpunkt kann man aufwendige Initialisierungen machen bzw. Dateien nachladen, die man später braucht.
Wie wendet man diese "Stylesheets" nun an? Ganz einfach, man packt alle Module, die "gestyled" werden sollen, in ein style-Element:
<style apply="output" expr="$stylesheet[ $Sstyle ]"> <module name=""><phtml><![CDATA[ <p/><?slink __"To the edito... <p/><?slink __"Edit projec... ]]></phtml></module> ... </style>
Das apply bezieht sich auf den Zeitpunkt, zu dem das Stylesheet angewendet werden soll. apply="output" bestimmt, dass das Stylesheet kurz vor der Ausgabe angewendet werden soll. Normalerweise würde man nun den Dateinamen des Stylesheets mit src="pfad" angeben, da wir aber zwischen zwei Stylesheets hin- und herschalten wollen, geben wir mit expr einen Perl-Ausdruck an, der ein Stylesheet-Objekt als Ergebnis haben (sollte). Das muss natürlich auch kein XSLT-Stylesheet sein, aber wem sage ich das...
Das Modul ist übrigens (bis auf die durch "..." angedeuteten Lücken) vollständig, d.h. der Kopf mit Titel und Sprachumschalter (sowie Stylesheet-Umschalter) wird durch das Stylesheet hinzugefügt.
4. Tinychat - ein kleiner Chat in 20 Zeilen
Zum Schluss noch ein sehr einfaches Beispiel: macro/tinychat.papp ist ein sehr einfaches Chat-Fenster: Es zeigt die letzten fünf Eingabezeilen an, gefolgt von einer Eingabebox. Der Chat-Inhalt ist systemweit, d.h. auf allen Servern in einem PApp-System ist immer derselbe Text, man kann diese "Chatbox" also in beliebige Programme einbauen.
<macro name="tinychat*()"><pxml><![CDATA[ #if $Atinychat_submit && $Pinput && !reload_p <: lockenv my $r = getenv "TINYCHAT"; shift @$r while @$r > 5; push @$r, escape_html sprintf "%s: %s", username || "<ANON$userid>", $P{input}; setenv "TINYCHAT", $r; ; :> #endif <: my $r = getenv "TINYCHAT"; echo map "<tt>$_</tt><br />", @$r; :> <br /> <?sform -tinychat_submit => 1:>__"Chat: "<?textfield "input":><?endform:> ]]></pxml></macro>
Zuallererst ist tinychat nur eine Funktion, teilt sich also den Namensraum mit dem Aufrufer. Tinychat ist so winzig und so schlecht konfigurierbar, dass das Sinn macht... Sehen wir uns mal den Teil an, der die Ausgabe erledigt:
<: my $r = getenv "TINYCHAT"; echo map "<tt>$_</tt><br />", @$r; :>
Die Texte sind in einer "Environment"-Variable gespeichert. Diese Variablen sind global für das gesamte PApp-System und könenn auch ausserhalb von PApp abgefragt bzw. verändert werden. Das ganze ist also eher ein System zur asynchronen Kommunikation. Neben normalen Strings kann man alles darin ablegen, was irgendwie serialisierbar ist, insbesondere eine Array-Referenz, in der die einzelnen Zeilen sind.
Zur Ausgabe wird also lediglich die Variable PAPP_TINYCHAT ausgelesen und die einzelnen Zeilen in ein "<tt>zeile</tt><br />" gepackt.
Fehlt noch das Eingabefeld:
<?sform -tinychat_submit => 1:> __"Chat: " <?textfield "input":> <?endform:>
Hier wird kein editform benutzt: Der Overhead ist im Vergleich zum Gewinn (es gibt keinen) zu gross. Die Funktion sform (die auch von ef_begin benutzt wird) gibt das einleitende FORM-Tag aus. Dann folgt der Text Chat: und ein ganz normales HTML-INPUT-Element. endform schleisslich gibt ein "</FORM>" aus und existiert eigentlich nur aus Symmetrie.
Das Eingabefeld heisst input. Was passiert, wenn wir sonst noch ein input-Feld haben und dieses andere Feld submittet wird? Kein Problem: Wir übergeben sform einfach ein Argument, das nur dazu dient, die Information "Tinychat-Formular ist gemeint" zu übertragen.
Ganz nebenbei: sform ist - wie sehr viele PApp-Funktionen - sehr einfach definiert:
PApp::HTML::_tag "form", method => 'GET', action => &surl ;
Nun zum Teil, der die Eingabezeile nach dem Abschicken hinzufügt:
#if $Atinychat_submit && $Pinput && !reload_p <: lockenv my $r = getenv "TINYCHAT"; shift @$r while @$r > 5; push @$r, escape_html sprintf "%s (%s): %s", username || "<ANON$userid>", $P{input}; setenv "TINYCHAT", $r; ; :> #endif
Die erste Zeile testet drei Dinge:
- Es muss "unser" Formular sein; das erkennt man daran, dass der Parameter tinychat_submit logisch wahr ist.
Die Eingabe sollte nicht leer sein (o.k. "0" ist auch nicht erlaubt).
Die Seite sollte nicht das Ergebnis eines "Reloads" sein.
Der letzte Punkt bedarf einer Erklärung: PApp weiss, wie oft eine Seite angefordert wurde und teilt dies über die Funktion reload_p mit, die die Anzahl der Seitenaufrufe für dieselbe Seite minus eins zurückliefert. Ist diese Zahl ungleich null, wurde der Code schon ausgeführt.
Das eigentliche hinzufügen ist Standard: Variable holen, alte Zeilen rauslöschen, neue Zeile hinzufügen, Variable auf neuen Wert setzen.
Die Funktion lockenv, in die die Manipulation eingeschlossen ist, schützt das Programm gegen gleichzeitige Modifikationen anderer Webserver, d.h. die Operation wird atomar. escape_html quoted das Argument. Die Funktion username (aus dem macro/admin-Paket) liefert den Namen des Benutzers, falls dieser einen besitzt. Ansonsten nimmt Tinychat ANON + die numerische User-ID, die jedem Benutzer zugeteilt wird.
5. Die Nachteile
Bei allen Vorteilen, es gibt auch Nachteile... die packe ich ans Ende und fasse mich auch gerne sehr kurz:
5.1. Die Lizenz (Oder doch ein Vorteil?)
Tja, PApp war doch tatsächlich mal GPL, und zwar zu einer Zeit, zu der wir es praktisch nur als CGI-Krücke verwendet haben. Inzwischen hat sich PApp gemausert und wurde zu einem unserer Standbeine. Als eine andere Firma versuchte, uns mit unserem Produkt Konkurrenz bei unseren Kunden zu machen ("wir können da einfach ein paar Dutzend Programmierer dransetzen"), mussten wir leider handeln.
Die "PApp Public License" ist so ähnlich wie die MySQL Public License, d.h. wer sie privat einsetzt (bzw. für die Forschung und Lehre oder für eine not-for-profit-Organisation), darf PApp weiterhin kostenlos nutzen. Wer kräftig Kohle damit macht, muss uns einen Teil davon abgeben (die eigentliche Lizenz ist etwas länger ;).
Langfristig ist geplant, PApp wieder in GPL oder besser zu überführen. Mittelfristig muss man damit Leben ;)
5.2. Die Abhängigkeiten
Zur Zeit gibt es keine Perl-Release, die annähernd mit UTF8 zurechtkommt. Z.Zt. (d.h. buchstäblich in dieser Minute) benötigt PApp perl-5.7.0-DEVEL7952 oder ein paar hundert Patches davor oder danach. Demnächst wird auf DEVEL8xxx umgestellt (z.Zt. gibt es einige Bugs, die dies verhindern). PApp funktioniert zwar wunderbar, aber eben nur, wenn die restlichen Komponenten aufeinander abgestimmt sind.
5.3. MySQL
Das Grundsystem von PApp benötigt zwar kein MySQL, einige Module (z.B. PApp::Env) dagegen (aus Geschwindigkeitsgründen) schon. Möchte man also eine einfache Installation und alle Features empfiehlt sich MySQL, zumindest für die PApp-interne Datenbank selbst. Ansonsten arbeitet PApp mit allen Datenbanken, die ein DBI-Interface aufweisen, ohne Probleme.
5.4. Geschmackssache
PApp ist etwas sehr persönliches - ich habe meine eigenen Vorstellungen davon, wie Web/CGI etc. funktionieren sollte, darin verwirklicht. Es hat meine Motivation (die sich mit "nie wieder CGI" zusammenfassen liess) gewaltig gesteigert - Ein einfaches aber dennoch komplettes Content-Management-System kann man in weniger als 500 Zeilen hinlegen - für mich ein wichtiger Faktor, denn ich hasse nichts mehr, als das Rad jedesmal neu erfinden zu müssen.
Da ich bekannt bin für meinen etwas merkwürdigen Geschmack (sagt man mir) muss das wie nicht unbedingt jedem gefallen.
A. Referenzen
- http://papp.plan9.de/. Die PApp-Homepage.
B. XSLT-Quelltext "Text-Only-Layout"
Dies und das folgende XSLT-Stylesheet kratzen nur an der Oberfläche dessen, was mit XSLT möglich ist.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:papp="http://www.plan9.de/xmlns/papp" > <xsl:output method="html" omit-xml-declaration='yes' media-type="text/html" encoding="utf-8"/> <xsl:template match="papp:module"> <html> <title><xsl:value-of select="@module"/></title> <body> <?slink "English", SURL_SET_LOCALE('en'):> <xsl:text> </xsl:text> <?slink "Deutsch", SURL_SET_LOCALE("de"):> <br/> <?slink __"Fancy", style => 1:> <hr/> <xsl:apply-templates/> <hr/> <:debugbox:> </body> </html> </xsl:template> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates/> </xsl:copy> </xsl:template> </xsl:stylesheet>
C. XSLT-Quelltext "Superbunt und Superhässlich-Layout"
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:papp="http://www.plan9.de/xmlns/papp" > <xsl:output method="xhtml" omit-xml-declaration='yes' media-type="text/html" encoding="utf-8"/> <xsl:template match="papp:module"> <html> <title><xsl:value-of select="@module"/></title> <body link="#a000000" alink="#a00000" vlink="#a00000" text="black" bgcolor="#ffffb4"> <?slink "English", SURL_SET_LOCALE('en'):> <xsl:text> </xsl:text> <?slink "Deutsch", SURL_SET_LOCALE("de"):> <br/> <?slink __"Plain", style => 0:> <table bgcolor='#ffff00' border="1"><tr> #if $PApp::module ne "" <td><?slink __"[MAIN PAGE]", "":></td> #endif #if $PApp::module ne "editor" or $Sprojectid <td><?slink __"[PROJECTS]", "editor", projectid => undef:></td> #endif </tr></table> <xsl:if test="@module=''"> <h1>__"Demo"</h1> __"Welcome to our pages..." </xsl:if> <table bgcolor="#ffffff" border="5" cellpadding="20"><tr><td> <xsl:apply-templates/> </td></tr></table> <hr/> <font size="1">Copyright whatever, whenever etc...</font> </body> </html> </xsl:template> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates/> </xsl:copy> </xsl:template> </xsl:stylesheet>