Teil von SELFHTML aktuell Teil von Artikel Teil von PHP

Henryk Plötz:
Threadbasiertes Forum mit PHP und MySQL

nach unten Henryk Plötz
nach unten Motivation
nach unten Vorbetrachtungen
nach unten Zeichenalgorithmus
nach unten Die Implementierung der Hauptdatei
nach unten Einträge lesen
nach unten Einträge schreiben
nach unten Verbesserungen
nach unten Forum online ausprobieren
nach unten Weitere Möglichkeiten
nach unten Fußnoten

Henryk Plötz

E-Mail: E-Mail henryk@ploetzli.ch
Homepage-URL: deutschsprachige Seite http://www.ploetzli.ch/

Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!

nach obennach unten

Motivation

Ein einfaches Forum, welches die Diskussionsbeiträge linear untereinander auflistet, ist gar nicht so schwer zusammenzubasteln, und viele benutzen so etwas. Wenn es aber daran geht, eine Forumssoftware zu schreiben, die auch Threads in ihrer Baumstruktur anzeigt, wird es etwas trickreicher. Dieser Artikel zeigt eine Möglichkeit, so ein Forum mit PHP und einer MySQL-Datenbank zu erstellen. Die Grundidee dazu habe ich vor einiger Zeit im SELFHTML-Chat aufgeschnappt und eine erste - noch ziemlich dreckige - Version irgendwann mal in 2 Stunden für eines meiner Projekte geschrieben, als ich grade Langeweile hatte.

Da im SELFHTML-Forum häufiger mal Fragen zu diesem Thema auftauchen und es im Forumsarchiv bis auf Verweise auf fix-und-fertige Software oder nur rudimentäre Ideen recht mager aussieht, habe ich mich dann entschlossen, diesen Artikel zu schreiben.

Zu den Vorkenntnissen:

Ich gehe davon aus, dass der Leser wenigstens ein bisschen mit PHP umgehen kann und grundlegende Kenntnisse von MySQL hat. Vor allem werden Kenntnisse darin, SQL-Queries direkt an den Datenbankserver zu senden, benötigt, um die Tabelle zu erstellen und die Testdaten einzufügen. Wer nur an den fertigen Skripten interessiert ist, sollte vielleicht besser gleich zum Ende scrollen, da vorher noch verschiedene Zwischenversionen auftauchen werden. Die engültigen Versionen sind jeweils:
nach unten index.php (Hauptdatei)
nach unten lesen.php (Eintragsansicht)
nach unten neu.php (Neue Einträge)
nach unten funktionen.php (Ausgelagerte Funktionen)
nach unten magic_quotes_fix.php (Sicherheitsfix).
Wer aber dann mit den Skripten Probleme hat, sollte den Artikel besser doch lesen :-)

nach obennach unten

Vorbetrachtungen

Zunächst einige Überlegungen dazu; welche Daten in der Datenbank abgelegt werden sollen.

Da es ein einfaches Forum für den Anfang werden soll, brauchen wir keine Benutzerverwaltung, sondern nur die reinen Forumsdaten. Dafür genügt eine einzelne Tabelle mit den Einträgen. Jeder Eintrag bekommt eine eindeutige Nummer, mit der er später angesprochen wird. Zu jedem Eintrag müssen auch Informationen über den Autor gespeichert werden, also Name und Email-Adresse. Ferner sollte der Zeitpunkt, zu dem der Eintrag erstellt wurde, abgelegt werden. Und schließlich braucht jeder Eintrag noch eine Betreffzeile und auch seinen Textinhalt.

Um die Baumstruktur zusammenzusetzen, benötigen wir die Information, welcher Eintrag auf welchen anderen Eintrag eine Antwort darstellt. Ich werde hier im Folgenden die Bezeichnungen Vater und Kind benutzen - letzteres ist eine Antwort auf ersteren. Jeder Tabelleneintrag erhält also eine Spalte für die ID-Nummer des Vaters. Außerdem brauchen wir noch eine Spalte, die eine eindeutige ID für jeden Thread erhält. Damit später einiges einfacher wird, verwenden wir für die Thread-ID einfach die eindeutige ID des ersten Eintrags in diesem Thread. Das bedeutet für die Datenbank keine Einschränkungen (nach unten [1]), erspart uns aber etwas Arbeit, da wir, wie wir sehen werden, zum "Zeichnen" eines Threads ohnehin die Nummer des ersten Eintrags brauchen.

SQL-Befehl zum Erstellen der benötigten Tabelle:

 CREATE TABLE `Forum` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`PID` INT DEFAULT '0' NOT NULL,
`TID` INT NOT NULL,
`Zeitpunkt` TIMESTAMP NOT NULL,
`AutorName` TEXT NOT NULL,
`Betreff` TEXT NOT NULL,
`AutorEmail` TEXT NOT NULL,
`Text` TEXT NOT NULL,
UNIQUE (`ID`)
);

Mit dem Statement CREATE TABLE wird die Tabelle für die Datensätze mit den Forumspostings definiert. ID ist die eindeutige Nummer des Postings, PID die Nummer des Vaters, und TID die Thread-ID.
Wie Sie ein solches SQL-Statement absetzen, entnehmen Sie der Hilfe zur MySQL-Datenbank. Eine gute deutschsprachige Anleitung ist auch das deutschsprachige Seite MySQL Datenbankhandbuch von Guido Stepken.

nach obennach unten

Zeichenalgorithmus

Damit ist die Datenstruktur festgelegt, und wir können uns mit dem Algorithmus zum "Zeichnen" des Forum-Baumes beschäftigen.

Rekursion

Vorher sollte ich vielleicht erst noch etwas zum Begriff der Rekursion sagen, da sich hier in der Vergangenheit auch immer wieder Verständnisschwierigkeiten aufgetan haben: Von "Rekursion" spricht man immer dann, wenn sich eine Funktion selbst wieder aufruft um einen Teil ihrer Arbeit zu erledigen. Das Standardbeispiel dafür ist die Fakultät, die als n! := n * (n-1)! und 0! := 1 definiert ist. Die Fakultät von 3 schreibt man also als 3! und berechnet sie zu:

3! = 3 * 2!
= 3 * 2 * 1!
= 3 * 2 * 1 * 0!
= 3 * 2 * 1 * 1
= 6

In Pseudocode würde man die Fakultät etwa so implementieren:

Fakultät in Pseudocode (rekursive Version):

FUNCTION fak(n)
BEGIN
  IF(n = 0) return 1;
  ELSE return fak(n-1) * n;
  ENDIF;
END

Diese Definition ist schön und einfach elegant. Man kann jede rekursive Funktion aber auch in einer iterativen Form schreiben. Dann ruft sich die Funktion nicht rekursiv wieder auf, sondern erledigt die gesamte Aufgabe selbst, meist mit einer Schleife. Die iterative Variation der Fakultät sähe in Pseudocode so aus:

Fakultät in Pseudocode (iterative Version):

FUNCTION fak(n)
BEGIN
  a := 1;
  FOR i := 1 TO n
    a := a * i;
  ENDFOR;
  return a;
END

Das ist nicht nur länger als die rekursive Variante, sondern es ist auch schwieriger auf den ersten Blick zu erfassen, was diese Funktion nun eigentlich soll. Für die meisten Probleme, die irgendwie rekursiv aussehen, ist die rekursive Lösung generell einfacher und übersichtlicher als die iterative Lösung. Allerdings arbeitet die iterative Lösung meist deutlich schneller.

Der Algorithmus

Was hat das mit unserem Forum zu tun? Nun, ein Forum mit Baumstruktur ist grade ein solches rekursives Problem: Ein Beitrag kann eine Menge von Antworten haben und jede dieser Antworten kann selbst wieder eine Menge von Antworten haben, und so weiter. Ich werde hier eine rekursive Lösung anbieten, da diese erstaunlich einfach ist. Die iterative Form bleibt als Übung für den Leser nach unten [2].

Die eigentliche Funktion zum zeichnen eines baumartigen Thread braucht nichts weiter zu tun, als eine Zeile mit den Informationen über den aktuell bearbeiteten Eintrag auszugeben und sich dann für jede der Antworten selbst wieder aufzurufen. Im Blockdiagramm etwa so:

Funktion zeichneBaum
(nimmt einen Parameter: eintrag)

gib eine Zeile mit Autor, Titel und Link aus

für jedes Kind kind von Eintrag eintrag

rufe Funktion zeichneBaum mit Parameter kind auf

nach obennach unten

Die Implementierung der Hauptdatei

Um die gleich folgende Implementierung der Forumshauptdatei zu testen, ohne zuvor die Teile für neue Nachrichten und Antworten zu schreiben, sollten wir erst einmal ein paar Testdaten in die Datenbank eingeben:

SQL-Befehle für die Testdaten:

INSERT INTO `Forum` (`ID`, `PID`, `TID`, `AutorName`, `AutorEmail`, `Betreff`, `Text`)
    VALUES ('1', '0', '1', 'Henryk Plötz', 'henryk@ploetzli.ch',
            'Test 1', 'Funktioniert das?');
INSERT INTO `Forum` (`ID`, `PID`, `TID`, `AutorName`, `AutorEmail`, `Betreff`, `Text`)
    VALUES ('2', '0', '2', 'Hans Dampf', 'hans@dampf.net',
            'Die Antwort auf die Frage nach Gott, dem Universum und dem ganzen Rest', '42');
INSERT INTO `Forum` (`ID`, `PID`, `TID`, `AutorName`, `AutorEmail`, `Betreff`, `Text`)
    VALUES ('3', '1', '1', 'Max Mustermann', 'max@mustermann.de',
            'Eine kleine Frage', 'Kann das Forum auch Threads darstellen?');
INSERT INTO `Forum` (`ID`, `PID`, `TID`, `AutorName`, `AutorEmail`, `Betreff`, `Text`)
    VALUES ('4', '3', '1', 'Henryk Plötz', 'henryk@ploetzli.ch',
            'Eine noch kleinere Antwort', 'Na klar');
INSERT INTO `Forum` (`ID`, `PID`, `TID`, `AutorName`, `AutorEmail`, `Betreff`, `Text`)
    VALUES ('5', '1', '1', 'Max Mustermann', 'max@mustermann.de',
            'Test', 'Na das wollen wir doch gleich mal testen.');

Auffällig ist, dass wir keinen Wert für den Zeitpunkt angegeben haben. MySQL ist dann so freundlich und nimmt automatisch die aktuelle Zeit.

Die Hauptdatei

Die Forumshauptdatei muss nun erstmal nicht viel mehr erledigen, als die Beschreibungen für alle Einträge aus der Datenbank zu holen (denn wir wollen ja auch alle Einträge anzeigen) und die zeichneBaum()-Funktion für jeden Thread einmal aufzurufen.

Um die Einträge zwischenzuspeichern, nachdem sie aus der Datenbank kommen, verwenden wir ein Array und legen jeden Eintrag an der Arrayposition ab, die seiner ID entspricht. Damit können wir dann die Einträge leicht wieder aus dem Array herausfischen.

Ein kleines Problem stellt sich noch: In der Datenbank stehen nur die Rückbezüge, also für jeden Eintrag ist sein Vater verzeichnet. Für die Zeichenfunktion wären aber Vorwärtsbezüge zumindest praktisch, wir müssen also für jeden Eintrag alle Kinder kennen. Ohne diese Information bliebe nur das Durchsuchen des Arrays mit den Eintragsdaten, um einen Eintrag zu finden, der den aktuell bearbeiteten Eintrag als Vater hat.

Damit das kurz und schmerzlos wird, legen wir ein zweidimensionales Array an, das in der ersten Dimension alle Einträge aufzeichnet und in der zweiten Dimension alle Kinder des jeweiligen Eintrags. Um dieses Array zu erstellen müssen wir in einer Schleife einmal über alle Einträge wandern. Wie ich bereits erwähnte, lesen wir ja sowieso alle Einträge aus der Datenbank aus, und können die dafür benötigte Schleife gleich mit benutzen.

Datum und Uhrzeit erfordern eine kleine Sonderbehandlung. In der Datenbank ist nur ein Timestamp abgelegt der etwas in der Art von 20020121230150 enthält. Das liest sich in der Forumshauptdatei sehr schlecht. Daher benutzen wir die MySQL-Funktion DATE_FORMAT(), um daraus eine lesbare Repräsentation zu gewinnen. DATE_FORMAT() nimmt einen Formatstring und einen Timestamp als Eingabe und spuckt dann den Timestamp gemäß dem Formatstring lesbar formatiert aus. Der Formatstring für eine Uhrzeit der Form 23:01:50 ist "%T" und der Formatstring für ein Datum der Form 21. 02. 2002 ist "%e. %m. %Y".

Hier die erste Version unserer Hauptdatei:

index.php (erste Version):

<?php
 $connid = mysql_pconnect("hostname", "dbuser", "dbpass");  // Datenbankverbindung herstellen
 if(!$connid) die("Die Datenbankserververbindung konnte nicht hergestellt werden");
 mysql_select_db("Forum") or die("Die Datenbank konnte nicht ausgewählt werden");

 unset($forumarray); // Variablen korrekt (de)initialisieren
 unset($kindarray);

  // Datenbank abfragen
 $result=mysql_query("SELECT ID, PID, TID, DATE_FORMAT(Zeitpunkt,'%T') AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt,'%e. %m. %Y') AS Datum,
                                    AutorName, Betreff FROM Forum
                        ORDER BY Zeitpunkt ASC", $connid);
 if(!$result) die("Die Datenbank konnte nicht abgefragt werden");

  // Ergebnisse einlesen
 while($tmp = mysql_fetch_array($result)) {  // Ergebnis holen
  $forumarray[ $tmp["ID"] ] = $tmp;          // Ergebnis im Array ablegen
  $kindarray[ $tmp["PID"] ][] =  $tmp["ID"]; // Vorwärtsbezüge konstruieren
 }

 mysql_free_result($result);  // Aufräumen

 // Die wichtigste Funktion hier
 function zeichneBaum($eintrag)
 {
  global $forumarray, $kindarray;  // Die hilfreichen Arrays importieren

  // Erstmal ein <li> aufmachen:
  ?><li><?php
   // Jetzt können wir eine Zeile mit den Infos zu dem durch
   // $eintrag bezeichneten Eintrag ausgeben
   ?>
    <?php echo htmlentities( $forumarray[$eintrag]["Betreff"] );?> von
    <?php echo htmlentities( $forumarray[$eintrag]["AutorName"] );?> am
    <?php echo htmlentities( $forumarray[$eintrag]["Datum"] );?> um
    <?php echo htmlentities( $forumarray[$eintrag]["Uhrzeit"] );?>
   <?php
   // Eventuell sind noch Kinder mit auszugeben:
   if(is_array($kindarray[$eintrag])) {       // Wenn das ein Array sein sollte, ...
    ?><ul><?php                               // ... dann mach ein <ul> auf, ...
     foreach($kindarray[$eintrag] as $kind) { // ... gehe alle Kinder durch ...
      zeichneBaum($kind);                     // ... und rufe für jedes Kind zeichneBaum() auf, ...
     }
    ?></ul><?php                              // ... und mach das <ul> wieder zu.
   }

   // Fertig
  ?></li><?php
 }

 // Jetzt kann der HTML-Teil beginnen
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
  <title>Mein erstes Forum</title>
 </head>
 <body>
  <h1>Mein erstes Forum</h1>
  <a href="neu.php">Neuer Eintrag</a> <!-- Der Link ist noch tot -->
  <ul>
   <?php
    if(is_array($kindarray)) {
     foreach($kindarray[0] as $thread) { // Für jedes Posting der obersten Ebene...
      zeichneBaum($thread);              // ... zeichneBaum() aufrufen
     }
    }
   ?>
  </ul>
 </body>
</html>

Wir rufen hier die zeichneBaum()-Funktion für jedes Posting auf der obersten Ebene separat auf, da es ja keinen Eintrag mit der Nummer 0 gibt, auf den diese Funktion verlinken könnte.

nach obennach unten

Einträge lesen

Funktionen auslagern

Das Zeichnen der Threads in der Hauptdatei funktioniert nun schon mal ganz gut. Widmen wir uns jetzt dem Lesen der einzelnen Einträge: Der Übersichtlichkeit halber sollten wir diese Funktionalität in eine eigene PHP-Datei auslagern. Um Schreibarbeit zu sparen und evt. später leichter etwas verändern zu können, bietet es sich ebenfalls an, die Funktion zeichneBaum() in eine eigene Datei auszulagern.

Außerdem sollte der Skript-Teil, der die Datenbankverbindung aufbaut, mit ausgelagert werden, da er unverändert sowohl in der Hauptdatei als auch in der Nachrichtenansicht benötigt wird. Das Konstrukt echo htmlentities($string) wird ebenfalls häufig benötigt und bekommt eine eigene Funktion. Diese Funktion lassen wir auch gleich noch stripslashes() erledigen, da wir im Weiteren von aktivierten magic_quotes ausgehen. Mehr dazu weiter unten.

Die Datei, die das Lesen der Einträge ermöglicht, nennen wir praktischerweise lesen.php. Sie bekommt als einzigen Parameter die Nummer des Eintrags, den sie anzeigen soll, in der Variable $eintrag übergeben. Die Links auf diese Datei haben demnach die Form <a href="lesen.php?eintrag=X">...</a> und sollen auch so von der zeichneBaum()-Funktion erzeugt werden.

Es entsteht also zunächst eine Datei funktionen.php mit folgendem Inhalt:

funktionen.php (erste Version):

<?php
 // Gibt $string aus und wandelt vorher alle für HTML besonderen Zeichen in Entities um
 function wp($string)
 {
  echo htmlentities(stripslashes($string));
 }

 // Stellt die Datenbankverbindung her
 function DBverbinden()
 {
  $connid = mysql_pconnect("hostname", "dbuser", "dbpass");  // Datenbankverbindung herstellen
  if(!$connid) die("Die Datenbankserververbindung konnte nicht hergestellt werden");
  mysql_select_db("Forum") or die("Die Datenbank konnte nicht ausgewählt werden");
  return $connid;
 }

 // Die wichtigste Funktion hier
 function zeichneBaum($eintrag)
 {
  global $forumarray, $kindarray;  // Die hilfreichen Arrays importieren

  // Erstmal ein <li> aufmachen:
  ?><li><?php
   // Jetzt können wir eine Zeile mit den Infos zu dem durch
   // $eintrag bezeichneten Eintrag ausgeben
   ?>
    <a href="lesen.php?eintrag=<?php wp( $forumarray[$eintrag]["ID"] );?>">
     <?php wp( $forumarray[$eintrag]["Betreff"] );?></a> von
    <?php wp( $forumarray[$eintrag]["AutorName"] );?> am
    <?php wp( $forumarray[$eintrag]["Datum"] );?> um
    <?php wp( $forumarray[$eintrag]["Uhrzeit"] );?>
   <?php
   // Eventuell sind noch Kinder mit auszugeben:
   if(is_array($kindarray[$eintrag])) {       // Wenn das ein Array sein sollte, ...
    ?><ul><?php                               // ... dann mach ein <ul> auf, ...
     foreach($kindarray[$eintrag] as $kind) { // ... gehe alle Kinder durch ...
      zeichneBaum($kind);                     // ... und rufe für jedes Kind zeichneBaum() auf, ...
     }
    ?></ul><?php                              // ... und mach das <ul> wieder zu.
   }

   // Fertig
  ?></li><?php
 }
?>

Die Eintragsansicht

Die Datei lesen.php hat jetzt mehrere Aufgaben: Sie muss den Datensatz des ihr als ID übergebenen Eintrags aus der Datenbank holen und dabei gleich überprüfen, ob die ID überhaupt gültig ist. Die Fehlerbehandlung für eine ungültige ID kann recht einfach ausfallen: Ein Redirect auf index.php, dann kann sich der Benutzer aus dem Forum eine Nachricht mit gültiger ID aussuchen.

Weiterhin wäre es praktisch, wenn lesen.php auch gleich den aktuellen Threadbaum zeichnet, damit der Besucher einfach weiterlesen kann. Dafür holt es sich die Thread-ID des aktuell bearbeiteten Eintrags aus dem soeben ausgelesenen Datensatz, holt sich dann die Betreffzeilen und Zusatzinformationen aller Einträge mit derselben Thread-ID aus der Datenbank und ruft schließlich zeichneBaum() auf. Wenn zeichneBaum() einfach nur mit der aktuellen Eintrags-ID gestartet werden würde, würden alle Kinder und Kindeskinder des aktuellen Eintrags gezeichnet und verlinkt. Dazu hätten wir uns aber gar nicht alle Einträge des aktuellen Threads aus der Datenbank holen müssen. Es wäre doch schön, wenn wir gleich den ganzen aktuellen Thread zeichnen könnten, damit der Nutzer nach eigenem Ermessen in dem Thread hin- und herlesen kann. Dazu bräuchten wir aber die ID des obersten Eintrags im aktuellen Thread. Und nun kuck mal einer schau: Die ist ja "zufälligerweise" gleich der bereits bekannten Thread-ID, wie wir weiter oben festgelegt haben.

Unsere neuen Versionen der Dateien sehen also so aus:

lesen.php (erste Version):

<?php
 include("funktionen.php");

 unset($Eintragsdaten); // Benutzte Variablen deinitialisieren
 unset($forumarray);
 unset($kindarray);

 $connid = DBverbinden();

 if( isset($eintrag) ) {     // Wenn $eintrag übergeben wurde..
  $eintrag = (int) $eintrag; // ... $eintrag erst mal zu einem Integer machen ..
  if( $eintrag > 0 ) {       // ... und schauen ob es größer als 0 ist ..
   $result=mysql_query("SELECT ID, TID, DATE_FORMAT(Zeitpunkt,'%T') AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt, '%e. %m. %Y') AS Datum, AutorName, AutorEmail,
                        Betreff, Text FROM Forum WHERE ID = ".$eintrag);
   if(!$result) die("Die Datenbank konnte nicht abgefragt werden");
   if( mysql_num_rows($result) > 0) {  // überprüfen ob ein Eintrag mit dieser ID in der Datenbank ist
    $Eintragsdaten = mysql_fetch_array($result); // Und ggbf. aus der Datenbank holen
   }
  }
 }

 if(!isset($Eintragsdaten)) {    // Irgendwas ist schiefgelaufen
  header("Location: index.php"); // Benutzer zur Startseite umleiten
  exit();                        // Skript beenden
 }

 // Den Eintrag hätten wir, jetzt lesen wir die Kopfzeilen aller Einträge in diesem Thread aus
 $Thread = $Eintragsdaten["TID"];

 $result = mysql_query('SELECT ID, PID, TID, DATE_FORMAT(Zeitpunkt,"%T") AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt,"%e. %m. %Y") AS Datum, AutorName, Betreff FROM Forum
                        WHERE TID = '.$Thread.' ORDER BY Zeitpunkt ASC');
 if(!$result) die("Die Datenbank konnte nicht abgefragt werden");

  // Ergebnisse einlesen
 while($tmp = mysql_fetch_array($result)) {  // Ergebnis holen
  $forumarray[ $tmp["ID"] ] = $tmp;          // Ergebnis im Array ablegen
  $kindarray[ $tmp["PID"] ][] =  $tmp["ID"]; // Vorwärtsbezüge konstruieren
 }

 mysql_free_result($result); // Aufräumen

 // Kommen wir zum HTML-Teil
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
  <title>Mein erstes Forum - Eintrag <?php wp($Eintragsdaten["ID"]);?> lesen</title>
 </head>
 <body>
  <h1>Eintrag <?php wp ($Eintragsdaten["ID"]);?> lesen</h1>
  <p>Dieser Eintrag wurde von <?php wp($Eintragsdaten["AutorName"]);?>
    (<a href="mailto:<?php wp($Eintragsdaten["AutorEmail"]);?>"><?php wp($Eintragsdaten["AutorEmail"]);?></a>)
    am <?php wp($Eintragsdaten["Datum"]);?> um <?php wp($Eintragsdaten["Uhrzeit"]);?> verfasst.</p>
  <h2><?php wp($Eintragsdaten["Betreff"]);?></h2>
  <p><?php echo nl2br(htmlentities(stripslashes($Eintragsdaten["Text"])));?></p>
  <h2>Ganzer Thread</h2>
  <ul>
   <?php
    zeichneBaum($Thread);  // zeichneBaum() für den obersten Eintrag des aktuellen Threads aufrufen
   ?>
  </ul>
  <a href="index.php">Zurück zur Forumshauptdatei</a>
  <a href="neu.php?eintrag=<?php wp($eintrag);?>">Eintrag beantworten</a>
 </body>
</html>

index.php (zweite Version):

<?php
 include("funktionen.php");

 unset($forumarray); // Variablen korrekt (de)initialisieren
 unset($kindarray);

 $connid = DBverbinden();

  // Datenbank abfragen
 $result=mysql_query("SELECT ID, PID, TID, DATE_FORMAT(Zeitpunkt,'%T') AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt,'%e. %m. %Y')
                        AS Datum, AutorName, Betreff FROM Forum
                        ORDER BY Zeitpunkt ASC", $connid);
 if(!$result) die("Die Datenbank konnte nicht abgefragt werden");

  // Ergebnisse einlesen
 while($tmp = mysql_fetch_array($result)) {  // Ergebnis holen
  $forumarray[ $tmp["ID"] ] = $tmp;          // Ergebnis im Array ablegen
  $kindarray[ $tmp["PID"] ][] =  $tmp["ID"]; // Vorwärtsbezüge konstruieren
 }

 mysql_free_result($result);  // Aufräumen

 // Jetzt kann der HTML-Teil kommen
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
  <title>Mein erstes Forum</title>
 </head>
 <body>
  <h1>Mein erstes Forum</h1>
  <ul>
   <?php
    if(is_array($kindarray)) {
     foreach($kindarray[0] as $thread) { // Für jedes Posting der obersten Ebene...
      zeichneBaum($thread);              // ... zeichneBaum() aufrufen
     }
    }
   ?>
  </ul>
 </body>
</html>

Wie beschrieben wird jetzt beim Lesen eines Eintrags der gesamte aktuelle Thread gezeichnet. Es gab im bereichsübergreifende Seite SELFHTML-Forum bereits Beiträge die gegen diese zum Beispiel im bereichsübergreifende Seite Self Community Board implementierten Funktion sprachen, da es bei sehr großen Threads doch eine Menge an zusätzlich zu übertragenden Informationen liefert. Wenn wir also die Art des SELFHTML-Forums einführen wollten, müssten wir nichts weiter tun, als den Aufruf von zeichneBaum($Thread); aus lesen.php zu löschen und durch die Schleife aus index.php (mit umgebenden <ul>!) zu ersetzen. Wobei dann aus $kindarray[0] ein $kindarray[$eintrag] gemacht werden müsste.

Weitere Variationen sind denkbar und einfach zu realisieren: Etwa das Zeichnen des Threadbaums vom Vater des aktuellen Eintrags aus, was den Rückschritt um eine Ebene ermöglicht, ohne die übertragene Datenmenge sinnlos zu erhöhen.

nach obennach unten

Einträge schreiben

Nun fehlt noch die Funktionalität zum Hinzufügen von Antworten bzw. neuen Threads. Diese beiden Funktionen sind verwandt genug, um sie in eine einzige PHP-Datei zu packen: neu.php .

Was soll diese Datei leisten? Wenn man ihr die Daten eines neuen Eintrags übergibt, sowie die Nummer eines existierenden Eintrags, soll sie einen neuen Eintrag anlegen, und zwar als Kind des übergebenen, schon vorhandenen Eintrags.

Wenn die Nummer des übergebenen Eintrags 0 ist, soll sie den neuen Eintrag als neuen Thread anlegen.

Wenn man der Datei gar nichts übergibt oder die übergebene Eintragsnummer nicht existiert, soll sie ein Formular zum Verfassen eines neuen Eintrags anzeigen, gegebenenfalls mit vorausgefüllten Feldern (je nachdem, welche Daten bereits übergeben wurden) sowie eine kleine Fehlermeldung (wenn es denn ein Fehler ist). Wenn man ihr nur die Nummer eines bestehenden Eintrags übergibt, soll sie diesen auslesen und die Formularfelder mit den Daten aus diesem Eintrag vorbelegen, wobei der Text des Postings mit dem üblichen Quotezeichen (">") als Zitat gekennzeichnet sein soll.

Das Anlegen eines neuen Eintrags als Kind eines anderen ist nicht besonders knifflig: Einfach den Datensatz des vorhandenen Eintrags auslesen und die Thread-ID merken. Dann den neuen Eintrag mit den übergebenen Daten und der gemerkten Thread-ID in die Datenbank schreiben. Eintrags-ID und Timestamp werden von MySQL automatisch vergeben.

Das Anlegen eines neuen Threads enthält einen kleinen Trick: Die Thread-ID ist nicht bekannt, da sie ja der Eintrags-ID entsprechen soll, und diese von MySQL erst beim Schreiben in die Datenbank vergeben wird. Daher muss der neue Eintrag erstmal ohne Thread-ID geschrieben werden, die vergebene Eintrags-ID gelesen und dann die Thread-ID vom Eintrag der gemerkten Eintrags-ID auf die gemerkte Eintrags-ID gesetzt werden.

Das klingt erstmal kompliziert, lässt sich in MySQL aber recht einfach lösen:
UPDATE Forum SET TID=ID, Zeitpunkt=Zeitpunkt WHERE ID=LAST_INSERT_ID().

Diese simple Abfrage erledigt das schon: Die Tabelle Forum wird aktualisiert, und zwar an der Stelle, wo wir zuletzt eingefügt haben (LAST_INSERT_ID()). Dort wird die TID auf die ID gesetzt. Das Zeitpunkt=Zeitpunkt ist lediglich nötig, weil MySQL normalerweise bei Updates die erste Timestamp-Spalte ebenfalls aktualisiert und auf den aktuellen Zeitpunkt setzt. Das wollen wir aber gar nicht. Eigentlich wäre es auch egal, da zwischen dem Einfügevorgang und dem Aktualisieren nur wenige Millisekunden vergangen sind (sein sollten), aber prinzipiell ist es besser, wenn wir das hinschreiben, was wir wirklich meinen, und das schließt in diesem Fall die Aktualisierung des Timestamps nicht ein.

Unsere neu.php sieht also so aus:

neu.php (endgültige Version):

<?php
 include("funktionen.php");     // Nützliche Funktionen importieren

 unset($errors);  // Ein Array in das wir Fehlermeldungen schreiben
 unset($Thread);  // Variablen die evt. uninitialisiert benutzt werden, löschen

 $connid = DBverbinden(); // Datenbankverbindung herstellen

 if(!isset($eintrag) || $eintrag < 0) $eintrag = 0;
 else $eintrag = (int)$eintrag; // $eintrag auf einen vernünftigen Wert setzen

 if($bearbeitet != true && $eintrag != 0) { // Es wurden keine Eingaben gemacht, und es soll eine Antwort verfasst werden
  $result = mysql_query("SELECT Betreff, Text FROM Forum WHERE ID =".$eintrag, $connid);
  if(!$result) die("Datenbank konnte nicht abgefragt werden");
  if(mysql_num_rows($result) != 1) { // Eintrag entweder nicht vorhanden oder mehrere Einträge mit derselben ID (hmm?)
   $errors[] = "Der Eintrag auf den Sie antworten wollen ist nicht in der Datenbank.
                Entweder existierte er nie und Sie spielen grade an den Formularparametern rum
                oder er wurde in der Zwischenzeit gelöscht.
                Wenn Sie dieses Formular abschicken, wird ein neuer Thread eröffnet.";
   $eintrag = 0; // Auf "neuen Thread" setzen
  } else {
   $eintragsdaten = mysql_fetch_array($result);
   $betreff = $eintragsdaten["Betreff"];  // Alte Betreffzeile übernehmen
   $text = $eintragsdaten["Text"]; // Alten Text übernehmen
   if(get_magic_quotes_runtime())
    $text = stripslashes($text); // Die Slashes die beim Auslesen freundlicherweise hinzugefügt werden entfernen
   $text = wordwrap($text); // Nachrichtentext automatisch umbrechen
   $text = preg_replace("/^/m", "> ", $text); // Zitatzeichen an den Anfang jeder Zeile stellen
   $text = addslashes($text); // Gleiche Ausgangsbedingungen für alle Variablen wiederherstellen
  }
 }

 if($abschicken != "") { // Nachricht soll abgeschickt werden
  // Allgemeine Überprüfungen
  if(!isset($name) || $name == "")
   $errors[] = "Es wurde kein Name eingegeben. Bitte geben Sie einen Namen ein.";
  if(!isset($email) || $email == "")
   $errors[] = "Es wurde keine Email-Addresse eingegeben. Bitte geben Sie eine Email-Addresse ein.";
  else {
   $email = trim($email); // Leerzeichen vor und hinter der Email-Addresse abschneiden
   if(!preg_match("/^[^@]+@.+\.\D{2,5}$/", $email)) // Überprüfung ob die Email-Addresse das Format name@domain.tld hat
    $errors[] = "Die eingebene Email-Addresse sieht nicht richtig aus.";
  }
  if(!isset($betreff) || $betreff == "")
   $errors[] = "Es wurde keine Betreff-Zeile eingegeben. Bitte geben Sie eine Betreffzeile ein.";
  if($eintrag != 0) { // Es soll eine Antwort verfasst werden
   $result = mysql_query("SELECT TID FROM Forum WHERE ID=".$eintrag, $connid);
   if(!$result) die("Datenbank konnte nicht abgefragt werden");
   if(mysql_num_rows($result) != 1) { // Da ist irgendwas faul
    $errors[] = "Der Eintrag auf den Sie antworten wollen ist nicht in der Datenbank.
                 Entweder existierte er nie und Sie spielen grade am Formular rum
                 oder er wurde zwischenzeitlich gelöscht.
                 Wenn Sie das Formular erneut abschicken wird ein neuer Thread eröffnet.";
    $eintrag = 0;
   } else {
    list($Thread) = mysql_fetch_row($result);
   }
  }

  if(!isset($errors)) { // Keine Fehler bisher, let's rock
   if($eintrag == 0) $Thread = 0; // neuer Thread
   $result = mysql_query("INSERT INTO Forum (PID, TID, AutorName, Betreff, AutorEmail, Text)
      VALUES (".$eintrag.",".$Thread.",'".$name."','".$betreff."',
             '".$email."','".$text."')", $connid);
   if(!$result) die("Konnte den neuen Eintrag nicht in die Datenbank schreiben");
   if($eintrag == 0) // Jetzt die Thread-ID des neuen Threads korrekt setzen
    if(!mysql_query("UPDATE Forum SET TID=ID, Zeitpunkt=Zeitpunkt WHERE ID = LAST_INSERT_ID()", $connid))
     die("Konnte die Thread-ID in der Datenbank nicht aktualisieren.
         Die Datenbasis könnte inkonsistent sein.");

   // Wenn wir noch leben, dann ist alles glatt gegangen.
   header("Location: lesen.php?eintrag=". (int)mysql_insert_id()); // Benutzer auf den neuen Eintrag umleiten
   exit(); // Skript beenden
  }
 }

 // HTML-Teil
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
<?php if($eintrag == 0) { ?>
  <title>Neuen Eintrag verfassen</title>
<?php } else { ?>
  <title>Antwort verfassen</title>
<?php } ?>
 </head>
 <body>
<?php if($eintrag == 0) { ?>
  <h1>Neuen Eintrag verfassen</h1>
<?php } else { ?>
  <h1>Antwort verfassen</h1>
  <p>Im Formular ist der Eintrag auf den Sie antworten noch einmal komplett zitiert.
   Bitte löschen Sie beim Beantworten nicht benötigte Zitate.</p>
<?php } ?>
<?php if(isset($errors)) { /* es sind Fehler aufgetreten */?>
  <h2>Fehler:</h2>
  <p>Beim Bearbeiten Ihrer Anfrage sind folgende Fehler aufgetreten:</p>
  <ul>
<?php foreach($errors as $error) { /* alle Fehler durchgehen */ ?>
   <li><?php wp($error);?></li>
<?php } ?>
<?php } ?>
  </ul>
  <!-- Jetzt das Formular -->
  <h2>Nachricht schreiben</h2>
  <form action="neu.php" method="POST">
   <!-- Der Eintrag auf den geantwortet wird, oder 0 für neuen Thread -->
   <input type="hidden" name="eintrag" value="<?php wp($eintrag);?>">

   <!-- Damit die eingegeben Daten nicht aus der Datenbank überschrieben werden -->
   <input type="hidden" name="bearbeitet" value="true">

   <table>
    <tr>
     <td>Ihr Name: </td><td><input type="text" size="80" name="name" value="<?php wp($name);?>"></td>
    </tr>
    <tr>
     <td>Ihre Mailaddresse: </td><td><input type="text" size="80" name="email" value="<?php wp($email);?>"></td>
    </tr>
    <tr>
     <td>Betreff: </td><td><input type="text" size="80" name="betreff" value="<?php wp($betreff);?>"></td>
    </tr>
    <tr valign="top">
     <td>Nachrichtentext: </td>
     <td>
      <textarea cols="80" rows="10" wrap="virtual" name="text"><?php wp($text);?></textarea>
     </td>
    </tr>
    <tr>
     <td>
<?php if($eintrag == 0) { ?>
      <a href="index.php">Abbrechen<br><small>Zur Hauptseite</small></a>
<?php } else { ?>
      <a href="lesen.php?eintrag=<?php wp($eintrag);?>">Abbrechen<br><small>Zum gelesenen Eintrag</small></a>
<?php } ?>
     </td>
     <td><input type="submit" name="abschicken" value="Abschicken"></td>
    </tr>
   </table>
  </form>
 </body>
</html>

Damit ist die erste Version unseres Forums, die alle nötigen Funktionen beherrscht, fertig!

nach obennach unten

Verbesserungen

Bleiben noch einige Detailverbesserungen: In der Hauptdatei sind die Threads und die Einträge innerhalb der Threads nach dem gleichem Schema sortiert: neuere Beiträge kommen nach unten. Das ist für das Lesen zwar sehr schön, da man zum Erfassen des zeitlichen und des Sinn-Zusammenhangs jetzt einfach von oben nach unten lesen kann, wird bei eine größeren Hauptdatei jedoch sehr unschön, da die neueren Threads ganz unten landen.

Man könnte jetzt die Reihenfolge der Datenbankabfrage umkehren - aus dem ASC ein DESC machen - und hätte dann genau die umgekehrte Sortierung, wie sie etwa im SELFHTML-Forum vorliegt. Das wäre aber mindestens ebenso unschön, da jetzt zwar die neueren Threads oben wären, man zum Erfassen des Zusammenhangs innerhalb eines Threads aber zick-zack lesen müsste: Innerhalb jeder Ebene von unten nach oben und die Teilthreads von oben-links nach unten rechts.

Besser ist es die Threads in der Hauptdatei einfach anders herum zu sortieren, und die Sortierung innerhalb eines Threads so zu belassen. Das erledigt ein einfaches
$kindarray[0] = array_reverse($kindarray[0]);
indem es die Threads auf der obersten Ebene umdreht.

Die nächste Verbesserung betrifft die Eintragsansicht. Es wäre schön, wenn der aktuell gelesene Eintrag in der Threadübersicht unterhalb jedes Eintrags hervorgehoben würde, um die Orientierung zu erleichtern. Dazu ist es wohl am besten, die Nummer des aktuell gelesenen Eintrags im rekursiven Aufruf von zeichneBaum() einfach "mitzuschleppen", also beim ersten Aufruf dieser Funktion die Nummer des aktuellen Eintrags als Parameter anzugeben und diesen dann jedem rekursiven Aufruf mitzugeben.

Damit die Funktion weiterhin ohne Änderungen der Hauptdatei funktioniert, bedienen wir uns der schönen Eigenschaft von PHP, optionale Funktionsparameter zu übergeben.

In der Funktion selber wird dann abgeprüft, ob sie eine Zeile für den aktuell vom Benutzer betrachteten Eintrag ausgeben soll und dann keinen Link auf diesen Eintrag schreibt, sondern einfach nur eine fette Zeile mit den Eintragsdaten. Die neue zeichneBaum() sieht also so aus:

neue Version von zeichneBaum():

 function zeichneBaum($eintrag, $aktuellerEintrag = 0)
 {
  global $forumarray, $kindarray;  // Die hilfreichen Arrays importieren

  // Erstmal ein <li> aufmachen:
  ?><li><?php
   // Prüfen ob ein Link gesetzt werden soll oder nicht
   if($eintrag == $aktuellerEintrag) { // Kein Link
    ?>
     <strong><?php wp( $forumarray[$eintrag]["Betreff"] );?> von
     <?php wp( $forumarray[$eintrag]["AutorName"] );?> am
     <?php wp( $forumarray[$eintrag]["Datum"] );?> um
     <?php wp( $forumarray[$eintrag]["Uhrzeit"] );?></strong>
    <?php
   } else {
    // Jetzt können wir eine Zeile mit den Infos zu dem durch
    // $eintrag bezeichneten Eintrag und einem Link ausgeben
    ?>
     <a href="lesen.php?eintrag=<?php wp( $forumarray[$eintrag]["ID"] );?>">
      <?php wp( $forumarray[$eintrag]["Betreff"] );?></a> von
     <?php wp( $forumarray[$eintrag]["AutorName"] );?> am
     <?php wp( $forumarray[$eintrag]["Datum"] );?> um
     <?php wp( $forumarray[$eintrag]["Uhrzeit"] );?>
    <?php
   }
   // Eventuell sind noch Kinder mit auszugeben:
   if(is_array($kindarray[$eintrag])) {       // Wenn das ein Array sein sollte, ...
    ?><ul><?php                               // ... dann mach ein <ul> auf, ...
     foreach($kindarray[$eintrag] as $kind) { // ... gehe alle Kinder durch ...
      zeichneBaum($kind,$aktuellerEintrag);   // ... und rufe für jedes Kind zeichneBaum() auf, ...
     }
    ?></ul><?php                              // ... und mach das <ul> wieder zu.
   }

   // Fertig
  ?></li><?php
 }

Der Aufruf aus lesen.php muss dann entsprechend abgeändert werden.

magic_quotes

Weiter oben wurde erwähnt, dass wir im gesamten Skript von aktivierten magic_quotes ausgehen. Nun haben einige Webmaster diese Funktion allerdings aus unerfindlichen Gründen deaktiviert, vermutlich weil andere Skripte von deaktivierten magic_quotes ausgehen. Damit es hier zu keinen Sicherheitsrisiken kommt, sollten alle Skriptdateien noch die folgende Datei per include() aufrufen:

magic_quotes_fix.php (endgültige Version):

<?php
// Simuliert magic_quotes_gpc wenn es nicht aktiviert ist
function quote_array($array)
{
 if( is_array($array) ) {
  foreach($array as $key => $val)  {
   $array[$key]=quote_array($val);
  }
  return $array;
 } else {
  return addslashes($array);
 }
}

if(get_magic_quotes_gpc() == 0 ) {
 switch($REQUEST_METHOD) {
  case  "GET": $BACK_VARS=$HTTP_GET_VARS;
               $HTTP_GET_VARS = quote_array($HTTP_GET_VARS);
               foreach($HTTP_GET_VARS as $key => $val) {
                if($BACK_VARS[$key] == ${$key}) ${$key} = $val;
               }
               break;
  case "POST": $BACK_VARS=$HTTP_POST_VARS;
               $HTTP_POST_VARS = quote_array($HTTP_POST_VARS);
               foreach($HTTP_POST_VARS as $key => $val) {
                if($BACK_VARS[$key] == ${$key}) ${$key} = $val;
               }
               break;
 }
 if(is_array($HTTP_COOKIE_VARS)) {
  $BACK_VARS=$HTTP_COOKIE_VARS;
  $HTTP_COOKIE_VARS = quote_array($HTTP_COOKIE_VARS);
  foreach($HTTP_COOKIE_VARS as $key => $val) {
   if($BACK_VARS[$key] == ${$key}) ${$key} = $val;
  }
 }
}

set_magic_quotes_runtime(1);
?>

Erläuterung:

Auf den Inhalt dieser Datei will ich hier nicht weiter eingehen, da das eventuell Thema eines anderen Artikels sein wird. Aber die Funktion, die sie erfüllt, ist recht einfach: Es wird überprüft, ob magic_quotes_gpc eingeschaltet ist, und falls nicht, werden trotzdem alle übergebenen Daten mit addslashes() bearbeitet. Auf diese Art kann sichergestellt werden, dass alle Daten, die letztendlich im Skript ankommen, wirklich korrekt escaped sind.

Der Vollständigkeit halber hier noch die Dateien funktionen.php, index.php und lesen.php mit den oben besprochenen Änderungen. An neu.php hat sich nichts geändert und magic_quotes_fix.php kann von oben übernommen werden.

index.php (endgültige Version):

<?php
 include("funktionen.php");

 unset($forumarray); // Variablen korrekt (de)initialisieren
 unset($kindarray);

 $connid = DBverbinden();

  // Datenbank abfragen
 $result=mysql_query("SELECT ID, PID, TID, DATE_FORMAT(Zeitpunkt,'%T') AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt,'%e. %m. %Y') AS Datum, AutorName, Betreff FROM Forum
                        ORDER BY Zeitpunkt ASC", $connid);
 if(!$result) die("Die Datenbank konnte nicht abgefragt werden");

  // Ergebnisse einlesen
 while($tmp = mysql_fetch_array($result)) {  // Ergebnis holen
  $forumarray[ $tmp["ID"] ] = $tmp;          // Ergebnis im Array ablegen
  $kindarray[ $tmp["PID"] ][] =  $tmp["ID"]; // Vorwärtsbezüge konstruieren
 }

 mysql_free_result($result);  // Aufräumen

 if(is_array($kindarray)) $kindarray[0] = array_reverse($kindarray[0]); // Reihenfolge der Threads umkehren
 // Jetzt kann der HTML-Teil kommen
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
  <title>Mein erstes Forum</title>
 </head>
 <body>
  <h1>Mein erstes Forum</h1>
  <a href="neu.php">Neuer Eintrag</a>
  <ul>
   <?php
    if(is_array($kindarray)) {
     foreach($kindarray[0] as $thread) { // Für jedes Posting der obersten Ebene...
      zeichneBaum($thread);              // ... zeichneBaum() aufrufen
     }
    }
   ?>
  </ul>
 </body>
</html>

lesen.php (endgültige Version):

<?php
 include("funktionen.php");

 unset($Eintragsdaten); // Benutzte Variablen deinitialisieren
 unset($forumarray);
 unset($kindarray);

 $connid = DBverbinden();

 if( isset($eintrag) ) {     // Wenn $eintrag übergeben wurde..
  $eintrag = (int) $eintrag; // ... $eintrag erst mal zu einem Integer machen ..
  if( $eintrag > 0 ) {       // ... und schauen ob es größer als 0 ist ..
   $result=mysql_query("SELECT ID, TID, DATE_FORMAT(Zeitpunkt,'%T') AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt, '%e. %m. %Y') AS Datum, AutorName, AutorEmail,
                        Betreff, Text FROM Forum WHERE ID = ".$eintrag);
   if(!$result) die("Die Datenbank konnte nicht abgefragt werden");
   if( mysql_num_rows($result) > 0) {  // überprüfen ob ein Eintrag mit dieser ID in der Datenbank ist
    $Eintragsdaten = mysql_fetch_array($result); // Und ggbf. aus der Datenbank holen
   }
  }
 }

 if(!isset($Eintragsdaten)) {    // Irgendwas ist schiefgelaufen
  header("Location: index.php"); // Benutzer zur Startseite umleiten
  exit();                        // Skript beenden
 }

 // Den Eintrag hätten wir, jetzt lesen wir die Kopfzeilen alle Einträge in diesem Thread aus
 $Thread = $Eintragsdaten["TID"];

 $result = mysql_query('SELECT ID, PID, TID, DATE_FORMAT(Zeitpunkt,"%T") AS Uhrzeit,
                        DATE_FORMAT(Zeitpunkt,"%e. %m. %Y") AS Datum, AutorName, Betreff FROM Forum
                        WHERE TID = '.$Thread.' ORDER BY Zeitpunkt ASC');
 if(!$result) die("Die Datenbank konnte nicht abgefragt werden");

  // Ergebnisse einlesen
 while($tmp = mysql_fetch_array($result)) {  // Ergebnis holen
  $forumarray[ $tmp["ID"] ] = $tmp;          // Ergebnis im Array ablegen
  $kindarray[ $tmp["PID"] ][] =  $tmp["ID"]; // Vorwärtsbezüge konstruieren
 }

 mysql_free_result($result); // Aufräumen

 // Kommen wir zum HTML-Teil
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
 <head>
  <title>Mein erstes Forum - Eintrag <?php wp($Eintragsdaten["ID"]);?> lesen</title>
 </head>
 <body>
  <h1>Eintrag <?php wp ($Eintragsdaten["ID"]);?> lesen</h1>
  <p>Dieser Eintrag wurde von <?php wp($Eintragsdaten["AutorName"]);?>
  (<a href="mailto:<?php wp($Eintragsdaten["AutorEmail"]);?>"><?php wp($Eintragsdaten["AutorEmail"]);?></a>) am
  <?php wp($Eintragsdaten["Datum"]);?> um <?php wp($Eintragsdaten["Uhrzeit"]);?> verfasst.</p>
  <h2><?php wp($Eintragsdaten["Betreff"]);?></h2>
  <p><?php echo nl2br(htmlentities(stripslashes($Eintragsdaten["Text"])));?></p>
  <h2>Ganzer Thread</h2>
  <ul>
   <?php
    zeichneBaum($Thread,$eintrag);  // zeichneBaum() für den obersten Eintrag des aktuellen Threads aufrufen
   ?>
  </ul>
  <a href="index.php">Zurück zur Forumshauptdatei</a>
  <a href="neu.php?eintrag=<?php wp($eintrag);?>">Eintrag beantworten</a>
 </body>
</html>

funktionen.php (endgültige Version):

<?php
 include("magic_quotes_fix.php");

 // Gibt $string aus und wandelt vorher alle für HTML besonderen Zeichen in Entities um
 function wp($string)
 {
  echo htmlentities(stripslashes($string));
 }

 // Stellt die Datenbankverbindung her
 function DBverbinden()
 {
  $connid = mysql_pconnect("hostname", "dbuser", "dbpass");  // Datenbankverbindung herstellen
  if(!$connid) die("Die Datenbankserververbindung konnte nicht hergestellt werden");
  mysql_select_db("Forum") or die("Die Datenbank konnte nicht ausgewählt werden");
  return $connid;
 }

 // Die wichtigste Funktion hier
 function zeichneBaum($eintrag, $aktuellerEintrag = 0)
 {
  global $forumarray, $kindarray;  // Die hilfreichen Arrays importieren

  // Erstmal ein <li> aufmachen:
  ?><li><?php
   // Prüfen ob ein Link gesetzt werden soll oder nicht
   if($eintrag == $aktuellerEintrag) { // Kein Link
    ?>
     <strong><?php wp( $forumarray[$eintrag]["Betreff"] );?>
     von <?php wp( $forumarray[$eintrag]["AutorName"] );?> am <?php wp( $forumarray[$eintrag]["Datum"] );?>
     um <?php wp( $forumarray[$eintrag]["Uhrzeit"] );?></strong>
    <?php
   } else {
    // Jetzt können wir eine Zeile mit den Infos zu dem durch
    // $eintrag bezeichneten Eintrag und einem Link ausgeben
    ?>
     <a href="lesen.php?eintrag=<?php wp( $forumarray[$eintrag]["ID"] );?>"><?php wp( $forumarray[$eintrag]["Betreff"] );?></a>
       von <?php wp( $forumarray[$eintrag]["AutorName"] );?> am <?php wp( $forumarray[$eintrag]["Datum"] );?>
       um <?php wp( $forumarray[$eintrag]["Uhrzeit"] );?>
    <?php
   }
   // Eventuell sind noch Kinder mit auszugeben:
   if(is_array($kindarray[$eintrag])) {       // Wenn das ein Array sein sollte, ...
    ?><ul><?php                               // ... dann mach ein <ul> auf, ...
     foreach($kindarray[$eintrag] as $kind) { // ... gehe alle Kinder durch ...
      zeichneBaum($kind,$aktuellerEintrag);   // ... und rufe für jedes Kind zeichneBaum() auf, ...
     }
    ?></ul><?php                              // ... und mach das <ul> wieder zu.
   }

   // Fertig
  ?></li><?php
 }
?>

nach obennach unten

Forum online ausprobieren

Eine vollständige Online-Version dieses Forums findet sich zum Ausprobieren unter:
deutschsprachige Seite http://www.ploetzli.ch/forum-fa/v1.0/index.php!

nach obennach unten

Weitere Möglichkeiten

Das Forum sollte jetzt voll funktionsfähig sein und wartet geradezu auf weitere Experimente. Die Oberfläche in dieser Version ist bewusst spartanisch gehalten und sollte sich leicht anpassen lassen.

Der interessierte Leser möchte jetzt evt. eine Art "Schwanzabschneider" (Archivierungsfunktion) oder andere administrative Funktionen einbauen.

Eine Anmerkung zu noch zu der Benutzung von <ul>-Listen: Der Einfachheit halber wird die Threadansicht in Form einer <ul>-Liste gezeichnet. Das hat den Vorteil, dass wir uns nicht allzu sehr um die Einrückung kümmern müssen und im Prinzip die schwere Arbeit dem Browser überlassen. Sollte das mit dem gewünschten Design nicht vereinbar sein, muss eine andere Form der Einrückung her. Man müsste dann in zeichneBaum() einen weiteren Parameter mitschleppen, der die Einrücktiefe angibt und bei jedem rekursiven Aufruf erhöht wird. Die relevanten Bestandteile dieser Funktion wären dann etwa so:

alternative zeichneBaum()-Methode:

 function zeichneBaum($eintrag, $aktuellerEintrag = 0, $aktuelleTiefe = 0)
 {
  global $forumarray, $kindarray;  // Die hilfreichen Arrays importieren

  // Irgendwie eine Einrückung erzielen, zum Beispiel durch wiederholte &nbsp;
  for($i = 0; $i < $aktuelleTiefe; $i++) echo "&nbsp;&nbsp;&nbsp;&nbsp;";

  //[... Zeile mit den Eintragsdaten oder einem Link ausgeben ...]

  //[... Anfang der Schleife über alle Kinder ...]
    zeichneBaum($kind, $aktuellerEintrag, $aktuelleTiefe+1);
  //[... Rest der Funktion ...]
 }

nach obennach unten

Fußnoten

[1] Es sind Bedenken geäußert worden, ob man damit die maximale Anzahl der Threads nicht künstlich einschränkt, da ja nicht jede mögliche Thread-ID belegt werden wird. Dem ist aber nicht so, denn da die Thread-ID ja im selben Zahlenbereich gespeichert wird wie die Eintrags-ID, gibt es genauso viele Thread-IDs wie Eintrags-IDs. Und da ja jeder Thread mindestens einen Eintrag haben sollte, gibt es im Maximum so viele Threads wie Einträge.

[2] Implementierungshinweis: Es geht am einfachsten mit einem Stapel - zu neudeutsch Stack - zu implementieren: Man tut das was man jetzt schon tun kann (also die Links ausgeben) und packt das was man erst später erledigen kann (also der Baumstruktur folgen) auf den Stapel. Wenn man nichts mehr zu tun hat, holt man sich eine neue Aufgabe vom Stack. Das Verständnis dieser Vorgehensweise ist aber für diesen Artikel nicht wichtig. Der PHP-Interpreter macht übrigens intern etwa dasselbe wenn er die rekursiven Funktionen abarbeitet.

Teil von SELFHTML aktuell Teil von Artikel Teil von PHP

© 2007 bereichsübergreifende Seite Impressum