Forum mit PHP & MySQL

Es gibt viele Möglichkeiten ein Forum zu schreiben. Als faule Sau kann ich euch versichern das hier ist die Einfachste.

Ich habe vier Ebenen: Thema > Bereich > Beitrag > Antwort. Jede Ebene erhält eine eigene Tabelle. Die oberste Ebene, das Thema, exisitiert eigentlich nur der Übersicht wegen und hat daher auch nur zwei Zellen: id und name.

Die nächste Ebene bilden die Bereiche. Jedes Thema hat seine Bereiche. Um jetzt den einzelnen Themen ihre Bereiche zuzuordnen, fügen wir einfach die id eines Themas in einer eigenen Spalte bei den ihm zugeordneten Bereichen ein. Ich habe diese Spalte tid (Themen-ID) genannt.

Wenn wir später dann das Forum aus der Datenbank auslesen, können wir einfach mit einer Schleife alle Themen auflisten, und mit einer weiteren Schleife die Bereiche - und auch gleichzeitig angeben, dass jeder Bereich seinem Thema zugeordnet werden soll. Das machen wir ganz einfach indem wir sagen: "Lese jetzt hier alle Bereiche aus, deren tid gleich der id des vorangegangenen Themas war". Mit allen folgeden Ebenen machen wirs genau so - eigentlich extrem simpel oder?

Außerdem wird es echt einfach ganze Bereiche und auch Beiträge zu verschieben, denn man braucht nur in der tid eines Bereiches die id des alten Themas rauslöschen und sie durch die id des neuen ersetzen. Soviel zur Funktion, jetzt zum Quelltext:

Als erstes der SQL-Befehl, der eine neue Datenbank namens "test" und die vier benötigten Tabellen erzeugt. Wenn jemand die vier Tabellen einer bereits bestehenden Datenbak zuordnen will, braucht er nur die CREATE DATABASE test; und USE test; Zeilen weglassen, und muss am Beginn der forum.php bei $daba das "test" mit dem Namen seiner Datenbank ersetzen.

CREATE DATABASE test;
USE test;
 
CREATE TABLE themen
(
	id int(11) NOT NULL AUTO_INCREMENT,
	name varchar(255) NOT NULL,
	PRIMARY KEY (id)
);
 
CREATE TABLE bereiche
(
	id int(11) NOT NULL AUTO_INCREMENT,
	tid int(1) NOT NULL,
	name varchar(255) NOT NULL,
	PRIMARY KEY (id)
);
 
CREATE TABLE beitraege
(
	id int(11) NOT NULL AUTO_INCREMENT,
	bid int(1) NOT NULL,
	name varchar(255) NOT NULL,
	PRIMARY KEY (id)
);
 
CREATE TABLE antworten
(
	id int(11) NOT NULL AUTO_INCREMENT,
	gid int(1) NOT NULL,
	von varchar(255) NOT NULL,
	zeit varchar(255) NOT NULL,
	text longtext NOT NULL,
	PRIMARY KEY (id)
);

Nachdem wir dann diese vier Tabellen angelegt haben kommen wir zum Skript. Dieses stammt aus den Anfängen meiner PHP-Tutorien, damals habe ich Skripta gehasst die über mehrere Seiten gingen, heute empfehle ich jedoch jedem jede mögliche Funktion in eine eigene Datei auszulagern, das ist übersichtlicher, man findet Fehler viel leichter und einzelne Dateien kann man einfacher und schneller um neue Funktionen erweitern. Der Übersichtlichkeit wegen zuerst das Skript, dann die Erklärung.

<?php
	function fnag($zurück)
	{
		/*
		* fnag bedeutet "Feld Nicht AusGefüllt".
		* exitse Tabelle wird immer gezeigt wenn
		* jemand ein Feld leer lässt. Es ist eine
		* einfache Funktion exit nach der Variabel
		* $zurück verlang, exit den Verweis beinhaltet,
		* mit dem der benutzer zurück zum Formular kommt.
		*/
		$ausgabe = '<table>
	<tr>
		<td align="center">
			Ein Feld wurde nicht ausgefüllt
		</td>
	</tr>
	<tr>
		<td align="center">
			<a href="'.$zurück.'">zurück</a>
		</td>
	</tr>
</table>';
		return $ausgabe;
	}
 
	// Diese Funktion verhindert, dass jemand eure SQL-Querys mit seinen Angaben manipuliert
	function escape($text)
	{
		return mysql_real_escape_string(stripslashes($text));
	}
 
	$host = 'localhost'; //Host
	$bnzr = ''; //MySQL - Benutzer
	$pswt = ''; //Passwort
	$daba = ''; //Datenbank
 
	$ttab = 'themen'; //Tabelle mit den Themen
	$btab = 'bereiche'; //Tabelle mit den Foren
	$gtab = 'beitraege'; //Tabelle mit den topicS
	$atab = 'antworten'; //Tabelle mit den thReads
 
	//verbinden zum MySQL-Vorleger
	mysql_connect($host, $bnzr, $pswt) OR exit('MYSQL VERBINDUNG FEHLGESCHLAGEN');
 
	//verbinden zur Datenbank
	mysql_select_db($daba) OR exit('DATENBANK VERBINDUNG FEHLGESCHLAGEN');
 
	//THEMEN UND IHRE BEREICHE AUFLISTEN
	if (empty($_GET['aktion'])) {
		$themen = ''; // exitse Var reichern wir mit unseren Themen an
		$afge_thma = mysql_query('SELECT * FROM '.$ttab) OR exit('FEHLER<br>aktion::1');
		while ($astg_thma = mysql_fetch_assoc($afge_thma)) {
			$thema = '<tr><td>'.$astg_thma['name'].'</td></tr>';
	
			$bereiche = ''; // exitse Var reichern wir mit unseren Bereichen an
			$afge_brch = mysql_query('SELECT * FROM '.$btab.' WHERE tid='.$astg_thma['id']) OR exit('FEHLER<br>aktion::2');
			while($astg_brch = mysql_fetch_assoc($afge_brch)) {
				$bereiche .= '
<tr>
	<td>
		<a href="?aktion=bereich&id='.$astg_brch['id'].'">'.$astg_brch['name'].'</a>
	</td>
<tr>';
			}
			$themen .= $thema.$bereiche;
			unset($bereiche); //sonst hängt er exit vom Vorherigen ran
		}
 
		echo '<table width="100%">
	<tr>
		<td align="center">FORUM</td>
	</tr>
	<tr>
		<td>
			<table width="100%">
'.$themen.'
			</table>
		</td>
	</tr>
</table>';
	} else {
		//BEITRÄGE IM BEREICH AUFLISTEN
		if ($_GET['aktion'] == "bereich") {
			$afge_brch = mysql_query('SELECT * FROM '.$btab.' WHERE id='.$_GET['id']) OR exit('FEHLER<br>aktion:bereich:1');
			$astg_brch = mysql_fetch_assoc($afge_brch);
		
			$afge_thma = mysql_query('SELECT * FROM '.$ttab.' WHERE id='.$astg_brch['tid']) OR exit('FEHLER<br>aktion:bereich:2');
			$astg_thma = mysql_fetch_assoc($afge_thma);
		
			$beiträge = '';
			$afge_btrg = mysql_query('SELECT * FROM '.$gtab.' WHERE bid='.$astg_brch['id']) OR exit("FEHLER<br>aktion:bereich:3");
			while ($astg_btrg = mysql_fetch_assoc($afge_btrg)) {
				$beiträge .= '
	<tr>
		<td>
			<a href="?aktion=beitrag&id='.$astg_btrg['id'].'">'.$astg_btrg['name'].'</a>
		</td>
	</tr>';
			}
			$tid = $astg_thma['id'];
			$bid = $_GET['id'];
			echo '<table width="100%">
	<tr>
		<td align="center">'.$astg_brch['name'].'</td>
	</tr>
	<tr>
		<td width="100%">
			<table width="100%">
				<tr>
					<td width="50%">
						<a href="?aktion=#'.$astg_thma['id'].'">'.$astg_thma['name'].'</a> &raquo; <b>'.$astg_brch['name'].'</b>
					</td>
					<td width="50%" align="right">
						<a href="?aktion=neuer_beitrag&tid='.$tid.'&bid='.$bid.'">Neuer Beitrag</a>
					</td>
				</tr>
			</table>
		</td>
	</tr>
	<tr>
	<tr>
		<td>BEITRÄGE</td>
	</tr>
'.$beiträge.'
</table>';
		}
 
		//ANTWORTEN IN BEITRAG AUFLISTEN
		if($_GET['aktion'] == "beitrag") {
			$afge_btrg = mysql_query('SELECT * FROM '.$gtab.' WHERE id='.$_GET['id']) OR exit('FEHLER<br>aktion:beitrag:1');
			$astg_btrg = mysql_fetch_assoc($afge_btrg);
		
			$afge_brch = mysql_query('SELECT * FROM '.$btab.' WHERE id='.$astg_btrg['bid']) OR exit('FEHLER<br>aktion:beitrag:2');
			$astg_brch = mysql_fetch_assoc($afge_brch);
		
			$afge_thma = mysql_query('SELECT * FROM '.$ttab.' WHERE id='.$astg_brch['tid']) OR exit('FEHLER<br>aktion:beitrag:3');
			$astg_thma = mysql_fetch_assoc($afge_thma);
		
			$antworten = '';
			$afge_atwt = mysql_query('SELECT * FROM '.$atab.' WHERE gid='.$_GET['id'].' ORDER BY id ASC') OR exit('FEHLER<br>aktion:beitrag:4');
			while($astg_atwt = mysql_fetch_assoc($afge_atwt)) {
				//Ersetzt HTML-eigene in Ersatz-Zeichen
				$antwort = htmlspecialchars($astg_atwt['text']);
		
				//Text-Zeilenumbrüche in HTML Umbrüche umwandeln
				$antwort = nl2br($antwort);
		
				$antworten .= '<p>
	<table width="100%">
		<tr>
			<td width="25%" rowspan="2">'.$astg_atwt['von'].'</td>
			<td width="75%" align="right">'.date('d.m.Y - H:i:s', $astg_atwt['zeit']).'</td>
		</tr>
		<tr>
			<td colspan="2">'.$antwort.'</td>
		</tr>
	</table>
</p>';
		}
 
		echo'<table width="100%">
	<tr>
		<td align="center">'.$astg_btrg['name'].'</td>
	</tr>
	<tr>
		<td>
			<a href="?aktion=#'.$astg_thma['id'].'">'.$astg_thma['name'].'</a> &raquo;
			<a href="?aktion=bereich&id='.$astg_brch['id'].'">'.$astg_brch['name'].'</a> &raquo; <b>'.$astg_btrg['name'].'</b>
		</td>
	</tr>
	<tr>
		<td>
'.$antworten.'
		</td>
	</tr>
	<tr>
		<td>
			<b>ANTWORT:</b>
			<form method="post" action="?aktion=neue_antwort">
				<table width="100%">
					<tr>
						<td>Name:</td>
						<td><input name="name" size="25">
						<input type="hidden" name="gid" value="'.$astg_btrg['id'].'"></td>
					</tr>
					<tr>
						<td>Text:</td>
						<td><textarea name="text" rows="10" cols="50"></textarea></td>
					</tr>
					<tr>
						<td colspan="2">
							<input type="submit" value="antworten">
							<input type="reset" value="zurücksetzen">
						</td>
					</tr>
				</table>
			</form>
		</td>
	</tr>
</table>';
		}
 
		//NEUE ANTWORT SPEICHERN
		if ($_GET['aktion'] == "neue_antwort") {
			if ($_POST['text'] == "" OR $_POST['name'] == "") {
				echo fnag('?aktion=beitrag&id='.$_POST['gid']);
			} else {
				mysql_query('
					INSERT INTO
						'.$atab.'
					SET
						gid="'.escape($_POST['gid']).'",
						von="'.escape($_POST['name']).'",
						zeit='.time().',
						text="'.escape($_POST['text']).'"
				') OR exit("FEHLER<br>aktion:neue_antwort:1");
				header('Location:?aktion=beitrag&id='.$_POST['gid']);
			}
		}
 
		//NEUEN BEITRAG ERSTELLEN
		if ($_GET['aktion'] == 'neuer_beitrag') {
			$afge_brch = mysql_query('SELECT * FROM '.$btab.' WHERE id='.$_GET['bid']) OR exit('FEHLER<br>aktion:neuer_beitrag:1');
			$astg_brch = mysql_fetch_assoc($afge_brch);
			
			$afge_thma = mysql_query('SELECT * FROM '.$ttab.' WHERE id='.$astg_brch['tid']) OR exit('FEHLER<br>aktion:neuer_beitrag:2');
			$astg_thma = mysql_fetch_assoc($afge_thma);
			
			echo '<form method="post" action="?aktion=neuen_beitrag_speichern">
	<table width="100%">
		<tr>
			<td colspan="2" align="center">Neuen Beitrag erstellen</td>
		</tr>
		<tr>
			<td>Thema</td>
			<td>'.$astg_thma['name'].'</td>
		</tr>
		<tr>
			<td>Bereich</td>
			<td>'.$astg_brch['name'].'
				<input type="hidden" value="'.$_GET['bid'].'" name="bid">
				<input type="hidden" value="'.$astg_brch['name'].'" name="bereich">
			</td>
		</tr>
		<tr>
			<td>Beitragstitel</td>
			<td><input size="25" name="beitrag"></td>
		</tr>
		<tr>
			<td>Name:</td>
			<td><input size="25" name="name"></td>
		</tr>
		<tr>
			<td>Text:</td>
			<td><textarea name="text" rows="6" cols="57"></textarea></td>
		</tr>
		<tr>
			<td colspan="2">
				<input type="submit" value="erstellen">
				<input type="reset" value="zurücksetzen">
			</td>
		</tr>
	</table>
</form>';
		}
 
		//NEUEN BEITRAG SPEICHERN
		if ($_GET['aktion'] == 'neuen_beitrag_speichern') {
			if ($_POST['beitrag'] == '' OR $_POST['name'] == '' OR $_POST['text'] == '') {
				echo fnag('?aktion=neuer_beitrag&bid='.$_POST['bid']);
			} else {
				mysql_query('
					INSERT INTO
						'.$gtab.'
					SET
						bid="'.escape($_POST['bid']).'",
						name="'.escape($_POST['beitrag']).'"
				') OR exit('FEHLER<br>aktion:neuen_beitrag_speichern:1');
	
				$letzte_id = mysql_insert_id();
	
				mysql_query('
				INSERT INTO
						'.$atab.'
					SET
						gid=LAST_INSERT_ID(),
						von="'.escape($_POST['name']).'",
						zeit='.time().',
						text="'.escape($_POST['text']).'"
				') OR exit("FEHLER<br>aktion:neuen_beitrag_speichern:2");
				
				header('Location:?aktion=beitrag&id='.$letzte_id);
			}
		}
	}
?>

Vorausgesetzt man hat die richtigen Variabeln eingesetzt und die Tabellen korrekt erstellt, läuft das Skript fehlerfrei und kann bereits mit forum.php bestaunt werden.

Unsere forum.php ist nun zu sechs Aktionen fähig: thema (leere aktion), bereich, beitrag, neue_antwort, neuer_beitrag und neuen_beitrag_speichern. Diese Aktionen werden von einfachen if-Anweisungen gesteuert und dann ausgelöst, wenn wir über die Adresszeile die jeweiligen Werte für $_GET[aktion] mitgeben. Wollen wir also einen neuen Beitrag erstellen, geben wir einfach mit einem Fragezeichen die Variabel aktion mit und weisen ihr den Wert neuer_beitrag zu: forum.php?aktion=neuer_beitrag. So werden unsere Besucher wenn sie diesen Verweis auslösen dann von jener if-Anweisung abgefangen auf die dieser Wert zutrifft.

Da wir nicht bereits zu Beginn die forum.php mit einer Variabel ansteuern wollen, sondern schon der einfache Aufruf von forum.php eine Aktion auslösen soll, geben wir der ersten Aktion keinen Wert, also $_GET[aktion] == '', sodass immer diese Aktion ausgelöst wird wenn forum.php ohne mitgegebene Variabel angesteuert wird.

Zu den Aktionen:

1. Aktion: Themen und ihre Bereiche auflisten
Unsere Einstiegs-Funktion. Wenn der Besucher auf die Seite kommt, wird ihm zuerst aufgelistet welche Themen es im Forum gibt und in welche Bereiche sie unterteilt sind. Dies geschieht mit zwei verschachtelten Schleifen. Die Erste listet uns alle Themen aus der Datenbank auf. Jedem Thema sind jetzt aber noch Bereiche zugeordnet, die wir auch mitlisten wollen, deshalb suchen wir in der ersten Schleife, jedesmal wenn ein Thema aufgelistet wurde, die Bereiche-Tabelle nach allen Bereichen ab die zu dem Thema gehören und lassen sie dazu schreiben, so entsteht die Themenaufstellung bekannter Foren.

Vorgang:

Mit mysql_query schicken wir eine Anfrage an die Datenbank: SELECT * FROM $ttab. Das bedeutet, wir wollen den gesamten Inhalt der $ttab, also Thementabelle auswählen. Jetzt wo wir es ausgewählt haben lassen wir es uns in eine Liste eintragen, einen Array, sodass wir schnell und bequem darauf zugreifen können. Das geschieht mit mysql_fetch_assoc();.

Dieser Befehl ließt das Ausgewählte ein und schreibt es in die Liste, und zwar mit den Spaltennamen zur Identifikation. Die ganze Liste speichern wir in der Variabel $astg_thma ab. Wollen wir jetzt den Namen des ersten Eintrages auslesen, sprechen wir ihn mit $astg_thma[name] an, da sich dieser in der Spalte "name" befindet.

Zum Besseren Verständnis hier eine Darstellung so einer Liste:

$astg_thma
(
[id] => 1
[name] => Allgemein
)

Der Aufruf von $astg_thma[id] würde hier also "1" und von $astg_thma[name] "Allgemein" ausgeben.

Weil wir aber nicht nur eines, sondern alle eingetragenen Themen auflisten wollen, schreiben wir das ganze in eine Schleife, die while(). Der Inhalt den runden Klammern wird solange ausgeführt bis keine Einträge mehr in der Datenbank übrig sind. Wollen wir also das alle Themen in eine Liste ausgelesen werden, müssen wir while($astg_thma = mysql_fetch_assoc($afge_thma)) schreiben.

Weil die while jetzt aber mit jedem Durchlauf die Liste wieder überschreibt, sodass immer nur ein Thema in $astg_thma gelistet ist, müssen wir die Variabel gleich verwenden, und das geschicht innerhalb der geschwungenen {} Klammern nach der while()-Bedingung. Dort speichern wir den HTML-Quelltext, den wir damit später anzeigen wollen, in der Variabel $thema ab:

$thema = '<tr>
<td>'.$astg_thma[name].'</td>
</tr>';

Jetzt wo wir das Thema "herausen" haben, listen wir alle dazugehörigen Bereiche auf, mit derselben Prozedur, nur diesmal geben wir im mysql_query noch eine Bedingung dazu: WHERE tid='$astg_thma[id]'. Das bedeutet, dass wir in der Tabelle der Bereiche nun nur jene Bereiche auswählen, welche die id unseres aktuellen Themas in der Spalte tid stehen haben. Dann erfolgt wie oben eine Schleife, in der wir alle ausgewählten Einträge in eine Liste auslesen lassen, sodass PHP sie verwenden kann, und aus dieser Liste heraus speichern wir die Werte die wir anzeigen wollen gleich in der Variabel $bereiche ab.

Anschließend fügen wir $thema und $bereiche zusammen und speichern sie gemeinsam in der Variabel $auflistung ab. Mit jedem Durchlauf der ersten Schleife wird jetzt also ein Thema ausgelesen in $thema abgelegt und die zweite Schleife ausgelöst, die nach zugehörigen Bereichen sucht und sie dann in $bereiche ablegt. Zum Schluss werden die beiden, weil sie ja zusammen gehören, miteinander verbunden und mit jedem Gesamt-Durchlauf der Schleifen baut sich dann unsere Liste der Themen und ihrer Bereiche auf.

2. Aktion: Bereich
Wenn jetzt in der ersten, der leeren Aktion, jemand auf einen Bereich klickt, schicken wir ihn zur nächsten Aktion, der $_GET[aktion] == "bereich". Darin passiert das Ganze, aus der 1. Aktion noch einmal, nur jetzt in den beiden nächst rangtieferen Ebenen, nämlich den Bereichen und ihren Beiträgen.

In der Adresszeile geben wir gleich die id, des Bereichs mit, denn wir haben sie in der vorherigen Aktion bereits einmal ausgelesen und müssen es jetzt nicht unbedingt noch einmal tun.

Anhand dieser id suchen wir jetzt die ganze Beiträge-Tabelle nach Beiträgen ab, die dieser id zugeordnet worden sind, also wo bid (Bereichs-ID) gleich der id des ausgewählten Bereichs ist.

Neu hier ist auch der Neuer Beitrag Verweis. Wenn jemand darauf klickt, ruft er die aktion neuer_beitrag auf und schickt gleichzeitig zwei wichtige Variablen mit: tid und bid, sodass wir in der neuen Aktion gleich mit diesen Variablen weiterarbeiten können.

3. Aktion: Beitrag
Hier passiert das Selbe wie bei den Bereichen und den Themen: Zwei Tabellen, Werte auslesen und in einer Schleife solange hinschreiben bis alles da steht was in der Datenbank ist und den festgelegten Bedingungen entspricht. Das Antworten ist wieder ein Formular, diesmal halt nur am Ende hinzugefügt und nicht in einer eigenen Aktion.

4. Aktion: Antworten
Zu dieser Aktion wird ein Besucher geschickt, wenn er im Beitrag auf "erstellen" geklickt hat. Es werden die Variabeln die er dank des in der beitrag-Aktion eingebauten Formulars mitbringt auf ihre Ausfüllung überprüft und anschließend in die Antworten Tabelle eingetragen.

5. Aktion: neuer_beitrag
Hier wird einfach ein Formular aufgebaut und mit den mitgelieferten tid und fid kann ich gleich hinschreiben unter welchem Thema und in welchem Bereich der neue Beitrag eingetragen wird.

Das Formular funktioniert auch sehr einfach: Wir geben jedem Eingabefeld einen Namen, in unserem Fall beitrag, name und text. Wenn jetzt jemand etwas dort hineinschreibt, und auf erstellen drückt, wird das, was er in die Felder hineingeschrieben hat, den Namen der Felder als Variable hinzugedichtet.

Wenn ich also bei Name EaStErDoM reinschreib und auf submit erstellen, wird die Variable $_POST[name] = 'EaStErDoM'; deklariert. Als Aktion habe ich im Formular den Verweis zur Funktion neuen_beitrag_speichern angegeben, also wird der Ausfüllende mitsamt seinen Variablen dorthin geschickt.

6. Aktion: neuen_beitrag_speichern
In dieser Funktion findet jetzt das Einzige etwas Schwierige am Skript statt, ich muss nämlich gleichzeitig in zwei Tabellen - Beiträge und Antworten - eintragen und muss noch dazu die id, die der Beitrag automatisch zugewiesen kriegt, bei der Antwort als gid eintragen, damit die Antwort später beim auslesen auch dem richtigen Beitrag zugewiesen wird.

Gemacht wird das mit der bequemen MySQL-Funktion LAST_INSERT_ID(). Die tut nichts anderes, als das sie jetzt bei meiner Antwort die zuletzt eingefügte id einträgt - in unserem Fall ist das die id des Beitrages.

Sonst läuft alles wie gehabt, nur das ich jetzt nicht markiere (SELECT), sondern einfüge (INSERT INTO).

Ich füge jetzt also die bid und den Beitragsnamen den der Ausfüllende vorher eingegeben hat ein und schreib bei id NULL hin, das heißt das nichts gemacht wird. Das tu ich weil MySQL die id sowieso automatisch einfüllt. Dann füge ich die Daten der Antwort in die Antworten-Tabelle ein und als Beitrags-ID bid die zuletzt eingefügte id - LAST_INSERT_ID().

Wer dieses Forumchen in sein Benutzer-System einbauen will braucht nur statt den Namens-Eingabe-Feldern die Benutzernamen oder wie ich es mache: die Benutzer-IDs auslesen und hinschreiben lassen.

Soviel von mir, danke für deine Aufmerksamkeit.