1. Aufbau eines Templatesystems
Bei einem Templatesystem versucht man den Bereich im PHP-Skript der für die Ausgabe zuständig ist von dem Teil des Skripts zu trennen der für den Rest zuständig ist. Dies entspricht in etwa dem einem Model View Controller. Die Bereiche für die Ausgabe werden dabei in extra Dateien ausgelagert. Solche Dateien werden dann Templates genannt. Mit so einer Trennung ist es viel einfacher die Ausgabe eines Skriptes zu verändern. Es wird nur die entsprechende Template-Datei bearbeitet, der Skript-Teil der für das Holen der Daten für das Template zuständig ist bleibt dabei unberührt. So kann z.B. ein Webdesigner die Template-Dateien nach seinen HTML- und CSS-Wissen bearbeiten ohne zu Wissen wie die Daten aus einer MySQL-Datenbank ausgelesen werden.
2. Templatesysteme in PHP
Im Internet gibt es genügend Templatesystem für PHP. Alle haben ihre Vor- Und Nachteile,
bieten Features an die andere nicht haben, haben Sicherheitsaspekte die andere nicht
implementiert haben und verwenden eine Syntax die die anderen nicht benutzen. In den
meisten Fällen wird jedoch vergessen das PHP selbst bereits eine Templateengine
ist. PHP wurde so entwickelt dass es zwischen HTML-Code verwendet werden kann, daher
auch die PHP Start- und Endtags <?php
und ?>
. Deswegen
verwenden wir hier PHP als Templatesystem indem wir unseren Programmcode entsprechend
strukturieren und include-Anweisungen verwenden.
3. Eigenes Templatesystem schreiben
Mit include-Anweisungen können wir andere PHP-Dateien in das aktuelle Skript laden und ausführen. Diese Technik verwenden wir für unsere Templates. Die Template-Dateien laden wir dann mit so einer include-Anweisung. Daher können wir in den Template-Dateien PHP-Code verwenden um diverse Variablen ausgeben. Dies heißt aber auch dass wir keine User-Templates laden können bzw. sollten. Wenn wir nun ein System schreiben für ein großes Projekt und für die User eigene Templates anbieten so können sie zwar ganz normal die Variablen ausgeben, aber eben auch beliebigen PHP-Code wie z.B. shell_exec oder mail ausführen. Daher sollten nur die Webautoren Zugriff auf die Templates haben.
Unser Templatesystem wird von der Startdatei index.php
verarbeitet. Wie bei
allen Projekten wird das Error-Reporting hochgeschraubt und diverse Funktionen, Klassen und
Variablen geladen.
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
include 'functions.php';
include 'classes.php';
include 'variables.php';
?>
Nun benutzen wir die GET-Variable section
für ein Array-Include.
Dabei speichern wir in dem Array $dateien
alle mögliche Dateinamen die
durch ein include geladen werden können. Der Index eines Arrayelements gibt an
bei welchem Wert von $_GET['section']
die Datei geladen wird.
<?php
// zum Beispiel in der variables.php angelegt
$dateien = array();
$dateien['news'] = 'news.php';
$dateien['forum'] = 'forum.php';
$dateien['downloads'] = 'dl.php';
// ...
?>
Mit isset und file_exists wird nun überprüft ob die entsprechenden Variablen, Arrayelemente und include-Dateien existieren, die dann auch mit include geladen wird.
<?php
$ret = 1; // speichert den rückgabewert von include, standardwert 1
if (isset($_GET['section'], $dateien[$_GET['section']])) {
if (file_exists('includes/'.$dateien[$_GET['section']])) {
$ret = include 'includes/'.$dateien[$_GET['section']];
} else {
$ret = "Include-Datei konnte nicht geladen werden: 'includes/".$dateien[$_GET['section']]."'";
}
} else {
// default bereich laden, news
$ret = include 'includes/'.$dateien['news'];
}
?>
Einmal sehen wir das hier eine Variable $ret
verwendet wird. Sie enthält den
Rückgabewert der include-Anweisung. Des Weiteren enthält es einige
Abfragen ob bestimmte Arrayindizes oder Dateien existieren. Wenn dies nicht der Fall ist
so wird der Standardbereich geladen, in diesem Fall ein Newsskript.
Für unsere Include-Dateien definieren wir nun wie der Rückgabewert aussehen muss. Im
Normalfall soll die Include-Datei ein assoziatives Array zurückliefern. Das Element mit dem
Index filename
gibt dabei den Dateinamen vom Template an, was dieses
Skript laden möchte. Das Element mit dem Index data
ist ein Array und enthält
die Daten die im Template verwendet werden können. Dort können dann z.B. die Newsbeiträge
gespeichert werden (nur die Daten, keine HTML-Codes). Im Falle eines Fehlers kann
die Include-Datei einen String zurückliefern. Dies wird dann als Fehlermeldung
interpretiert und soll vom Templatesystem entsprechend ausgegeben werden. Und zum
Schluss gibt es noch ein Fall dass die Include-Datei den Wert 1
zurückliefert.
Bei so einem Wert wurde einfach vergessen ein Array oder ein String zurückzuliefern da
im Skript keine return-Anweisung verwendet wurde. All diese Informationen
schreiben wir am Besten mit einem Kommentar in das Skript.
<?php
$ret = 1; // speichert den rückgabewert von include, standardwert 1
/*
* Die Include-Datei muss eine return Anweisung enthalten mit folgenden
* Werten:
* - Bei normaler Ausführung
* Array('filename' => string, -- Dateiname vom Template
* 'data' => Array()) -- Array mit Daten für das Template
* - Bei einem Fehler
* string -- Die Fehlermeldung die angezeigt werden soll.
*/
if (isset($_GET['section'], $dateien[$_GET['section']])) {
// ...
?>
Somit weiß man auch was eigentlich programmiert wurde und andere Entwickler können das System verstehen.
Nehmen wir an wir hätten nun solche Include-Dateien und müssen nun das richtige Template
ausgeben. Dafür verwenden wir diverse If-Abfragen um den Inhalt der $ret
Variable zu prüfen und die entsprechende Datei zu laden.
<?php
if (is_array($ret) and isset($ret['filename'], $ret['data']) and
is_string($ret['filename']) and
is_array($ret['data'])) {
// Gültiger return-Wert
if (file_exists($file = 'templates/'.$ret['filename'])) {
$data = $ret['data']; // speicher die Arraydaten in eine Variable $data
// die dann im Template verwendet werden kann.
include $file;
} else {
// Datei existiert nicht, eine Fehlermeldung anzeigen.
$data = array();
$data['msg'] = 'Templatedatei "'.$file.'" ist nicht vorhanden.';
include 'templates/error.tpl';
}
} else if (is_string($ret)) {
// include-Datei lieferte eine String zurück, welches eine Fehlermeldung sein soll
$data = array();
$data['msg'] = $ret;
include 'templates/error.tpl';
} else if (1 === $ret) {
// return-Anweisung wurde vergessen
$data = array();
$data['msg'] = 'In der Include-Datei wurde die return Anweisung vergessen.';
include 'templates/error.tpl';
} else {
// überhaupt ein ungültiger return-Wert
$data = array();
$data['msg'] = 'Die Include-Datei hat einen ungültigen Wert zurückgeliefert.';
include 'templates/error.tpl';
}
?>
Je nach Rückgabewert wird eine entsprechende Templatedatei geladen.
Im Normalfall wird die Templatedatei geladen die im Returnwert vom der
Include-Datei angegeben wurde und im Fehlerfall wird, wie hier angegeben, die
templates/error.tpl
-Datei geladen. Da in einem Fehlerfall
auch so eine Datei geladen werden kann wird diese nun mit dem folgendem
Inhalt erstellt.
<?php
/* Daten:
* 'msg' - Die Fehlermeldung
*/
?><p class="error">
<?php echo htmlspecialchars($data['msg']); ?>
</p>
Die Datei enthält einen PHP-Kommentar der beschreibt welche Daten
an die Templatedatei übergeben werden (mittels der Variable $data
).
In diesem Fall nur ein Feld msg
die die Fehlermeldung enthält.
Nun schreiben wir unsere Beispiel Include-Datei. Da das Newsskript der Bereich ist der geladen wird wenn ein ungültige Bereich gewählt wurde schreiben wir nun diesen Bereich. Dieses Kapitel behandelt jedoch nicht wie ein Newsskript erstellt wird, daher benutzen wir Pseudo-Daten.
<?php
$a = array();
$a['filename'] = 'news.tpl';
$a['data'] = array();
// Newsdaten irgendwie aus einer Datenbank holen
if (false) {
// Falls ein Datenbankfehler auftrat, kann hier nur simuliert werden, deswegen if (false)
return "Es trat ein Fehler in der Datenbank auf: ...";
}
$news = array();
// Array mit Newsbeiträgen füllen (normalerweise aus der Datenbank)
$newsbeitrag = array();
$newsbeitrag['Titel'] = 'Neue Homepage';
$newsbeitrag['Datum'] = '2008-01-01 00:00:00';
$newsbeitrag['Inhalt'] = 'Pünktlich zum Neujahr starten wir mit einer neuen Homepage.';
$news[] = $newsbeitrag;
$a['data']['news'] = $news;
return $a; // nicht Vergessen, sonst enthält $ret nur den Wert int(1)
?>
Zu beachten ist das hier je nach Situation ein String oder ein Array zurückgeliefert wird.
Dies ist auch die Idee mit den unterschiedlichen Rückgabetypen. Bei einem
Fehler kann einfach der Ausdruck return 'Fehlertext';
verwendet werden.
Im anderen Fall wird das vorher definierte Array zurückgeliefert.
Da dieses Skript eine Template-Datei news.tpl
laden möchten
erstellen wir nun eine solche Datei, die dann die Newsbeiträge anzeigt.
<?php
/*
* Daten:
* 'news' -- Array mit Newsbeiträgen mit folgendem Aufbau
* Array(
* 'Titel', -- Der Titel der News
* 'Datum', -- Erstelldatum
* 'Inhalt' -- Inhalt der News
* )
*/
?>
<?php if (count($data['news'])) { ?>
<?php foreach ($data['news'] as $beitrag) { ?>
<h2><?php echo htmlspecialchars($beitrag['Titel']); ?></h2>
<p class="beitrag">
<span>Geschrieben am <?php echo htmlspecialchars($beitrag['Datum']); ?></span>:
<?php echo htmlspecialchars($beitrag['Inhalt']); ?>
</p>
<?php } ?>
<?php } else { ?>
<p class="info">
Es sind keine News vorhanden
</p>
<?php } ?>
Am Anfang sieht man wieder einen informellen Kommentar wie das $data
-Array
aufgebaut ist. Dann folgt das eigentliche Template. Wie man sieht wechselt man hier immer
zwischen PHP- und HTML-Modus. Der PHP-Teil dient entsprechend nur dem Programmfluss und
den Ausgeben der Variablen. Der HTML-Code bestimmt dann entsprechend die HTML-Struktur.
Ein Template-Entwickler muss also nicht komplett PHP lernen, er muss nur Wissen
dass er für die Ausgabe immer <?php echo ... ?>
schreiben muss.
Er sieht diesen Code dann als Platzhalter und so ersetzt auch PHP diese Platzhalter
mit den Werten, in diesem Fall aus dem Array.
Neben den Bereich der angezeigt werden soll fehlt noch die komplette HTML-Struktur.
Die wird genauso vor und nach dem Laden des Templates mit include geladen.
Die komplette index.php
sieht dann wie folgt aus.
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
include 'functions.php';
include 'classes.php';
include 'variables.php';
$ret = 1; // speichert den rückgabewert von include, standardwert 1
// Laden der Include-Datei
/*
* Die Include-Datei muss eine return Anweisung enthalten mit folgenden
* Werten:
* - Bei normaler Ausführung
* Array('filename' => string, -- Dateiname vom Template
* 'data' => Array()) -- Array mit Daten für das Template
* - Bei einem Fehler
* string -- Die Fehlermeldung die angezeigt werden soll.
*/
if (isset($_GET['section'], $dateien[$_GET['section']])) {
if (file_exists('includes/'.$dateien[$_GET['section']])) {
$ret = include 'includes/'.$dateien[$_GET['section']];
} else {
$ret = "Include-Datei konnte nicht geladen werden: 'includes/".$dateien[$_GET['section']]."'";
}
} else {
// default bereich laden, news
$ret = include 'includes/'.$dateien['news'];
}
// Laden des HTML-Kopfs
include 'templates/html_header.tpl'; // Doctype, <html>, <head>, <meta> kram
include 'templates/html_body_tag.tpl'; // Analog auch einfach echo "<body>";
include 'templates/menu.tpl'; // falls man z.B. ein Menu haben möchte
// Laden der Template-Datei
if (is_array($ret) and isset($ret['filename'], $ret['data']) and
is_string($ret['filename']) and
is_array($ret['data'])) {
// Gültige Include-Datei
if (file_exists($file = 'templates/'.$ret['filename'])) {
$data = $ret['data']; // speicher die Arraydaten in eine Variable $data
// die dann im Template verwendet werden kann.
include $file;
} else {
$data['msg'] = 'Templatedatei "'.$file.'" ist nicht vorhanden.';
include 'templates/error.tpl';
}
} else if (is_string($ret)) {
// Fehlermeldung
$data['msg'] = $ret;
include 'templates/error.tpl';
} else if (1 === $ret) {
// return wurde vergessen
$data['msg'] = 'In der Include-Datei wurde die return Anweisung vergessen.';
include 'templates/error.tpl';
} else {
// ein Ungültiger Return wert
$data['msg'] = 'Die Include-Datei hat einen ungültigen Wert zurückgeliefert.';
include 'templates/error.tpl';
}
// HTML footer laden
include 'templates/html_footer.tpl'; // Zeug wie </body> und </html>
?>
Nun fehlen die diversen HTML-Dateien, aber die können nach den eigenen Wünschen geschrieben werden. Wenn nun ein neuer Bereich hinzugefügt werden soll geht man wie folgt vor.
-
Es wird eine Template-Datei angelegt. Sie enthält ganz oben einen Kommentar damit jeder weiß welche Variablen bzw. Werte in dieses Template übergeben wird. Anhand dieser Information kann dann das eigentliche Template geschrieben werden.
-
Es wird eine Include-Datei angelegt. Diese erstellt dann ein Array für den Dateinamen und die Daten, füllt den Datenbereich und liefert dann entweder ein String für eine Fehlermeldung zurück (z.B. "Geben sie eine Emailadresse ein") oder das Array mit der die Template-Datei geladen werden soll.
-
Das
$section
Array muss entsprechend erweitert werden dass nun auch die Include-Datei geladen werden kann.
4. Vor- und Nachteile dieses Templatesystems
Wie beschrieben kann dieses Templatesystem nicht für ungeprüfte Templatedaten wie Usertemplates verwendet werden. Die Sicherheit ist nicht gewährleistet dass die Benutzer auch nur das echo-Sprachkonstrukt verwenden. Vorteilhaft ist jedoch dass die Ausgabe etwas verzögert ist. Dies mag schlecht klingen jedoch ist es somit möglich ohne Probleme noch die Header-Angaben zu verändern, was mit den Funktionen session_start, header und setcookie entsprechend möglich ist.