Nicht jedes Projekt braucht eine Datenbank. Wenn ein einfaches Kontaktformular, ein Feedback-Bogen oder eine Umfrage nur wenige Einträge sammeln soll, reicht eine Textdatei völlig aus. PHP bringt alles mit, um Formulardaten zu empfangen, zu prüfen und in einer CSV-Datei zu speichern. Dieses Tutorial zeigt den kompletten Weg vom HTML-Formular bis zur gespeicherten Datei.
Das HTML-Formular
Als Ausgangspunkt dient ein einfaches Formular mit drei Feldern: Vorname, Nachname und E-Mail-Adresse. Die Daten werden per POST an dieselbe PHP-Datei gesendet.
<form action="" method="post">
<label for="vorname">Vorname:</label>
<input type="text" name="vorname" id="vorname"
required>
<label for="nachname">Nachname:</label>
<input type="text" name="nachname" id="nachname"
required>
<label for="email">E-Mail:</label>
<input type="email" name="email" id="email"
required>
<button type="submit" name="senden"
value="1">Absenden</button>
</form>
Das Attribut required sorgt dafür, dass der Browser leere Felder bereits vor dem Absenden abfängt. type="email" prüft zusätzlich, ob die Eingabe wie eine gültige E-Mail-Adresse aussieht. Diese clientseitige Validierung ersetzt aber nicht die serverseitige Prüfung in PHP.
Formulardaten empfangen und prüfen
Bevor die Daten gespeichert werden, müssen sie in PHP geprüft werden. Die Funktion trim() entfernt überflüssige Leerzeichen, filter_input() liest die POST-Werte sicher aus.
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vorname = trim($_POST['vorname'] ?? '');
$nachname = trim($_POST['nachname'] ?? '');
$email = trim($_POST['email'] ?? '');
$fehler = [];
if ($vorname === '') {
$fehler[] = 'Bitte Vorname eingeben.';
}
if ($nachname === '') {
$fehler[] = 'Bitte Nachname eingeben.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$fehler[] = 'Bitte gültige E-Mail eingeben.';
}
if (count($fehler) === 0) {
// Daten sind gültig, jetzt speichern
}
}
?>
Die Prüfung mit $_SERVER['REQUEST_METHOD'] ist zuverlässiger als einen bestimmten Submit-Button abzufragen. So funktioniert das Script auch dann, wenn das Formular per JavaScript abgesendet wird. filter_var() mit FILTER_VALIDATE_EMAIL prüft die E-Mail-Adresse nach den Regeln aus RFC 5322.
Daten mit file_put_contents() anhängen
Der einfachste Weg, eine Zeile an eine Textdatei anzuhängen, ist file_put_contents() mit dem Flag FILE_APPEND. Die Funktion erstellt die Datei automatisch, falls sie noch nicht existiert.
<?php
$datei = __DIR__ . '/daten/eintraege.txt';
$zeile = $vorname . ' | '
. $nachname . ' | '
. $email . ' | '
. date('d.m.Y H:i') . "\n";
file_put_contents($datei, $zeile, FILE_APPEND | LOCK_EX);
?>
Das Flag LOCK_EX setzt eine Dateisperre, damit bei gleichzeitigen Zugriffen keine Daten verloren gehen. Die Kombination FILE_APPEND | LOCK_EX ist der sicherste Weg, um Zeilen an eine Datei anzuhängen.
CSV-Format mit fputcsv()
Wenn die Daten später in Excel oder einem anderen Programm ausgewertet werden sollen, bietet sich das CSV-Format an. PHP hat dafür die Funktion fputcsv(), die sich automatisch um Trennzeichen und das korrekte Escaping von Sonderzeichen kümmert.
<?php
$csvDatei = __DIR__ . '/daten/kontakte.csv';
/* Kopfzeile schreiben, falls Datei neu ist */
$istNeu = !file_exists($csvDatei);
$handle = fopen($csvDatei, 'a');
if ($handle === false) {
die('Datei konnte nicht geöffnet werden.');
}
/* Dateisperre setzen */
flock($handle, LOCK_EX);
if ($istNeu) {
fputcsv($handle, [
'Vorname', 'Nachname', 'E-Mail', 'Datum'
], ';');
}
fputcsv($handle, [
$vorname,
$nachname,
$email,
date('d.m.Y H:i'),
], ';');
/* Sperre freigeben und Datei schließen */
flock($handle, LOCK_UN);
fclose($handle);
?>
Der zweite Parameter von fputcsv() bestimmt das Trennzeichen. Mit dem Semikolon (;) lassen sich die Dateien in deutschsprachigen Excel-Versionen direkt öffnen, da Excel in der deutschen Einstellung das Semikolon als Spaltentrennzeichen erwartet. Wenn ein Feld selbst ein Semikolon oder Anführungszeichen enthält, setzt fputcsv() automatisch Anführungszeichen um den Wert.
Komplettes Beispiel
Das folgende Script vereint Formular, Validierung und CSV-Speicherung in einer Datei. Es kann direkt auf einem Webserver getestet werden.
<?php
$csvDatei = __DIR__ . '/daten/kontakte.csv';
$meldung = '';
$fehler = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vorname = trim($_POST['vorname'] ?? '');
$nachname = trim($_POST['nachname'] ?? '');
$email = trim($_POST['email'] ?? '');
if ($vorname === '') {
$fehler[] = 'Vorname fehlt.';
}
if ($nachname === '') {
$fehler[] = 'Nachname fehlt.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$fehler[] = 'E-Mail-Adresse ungültig.';
}
if (count($fehler) === 0) {
$istNeu = !file_exists($csvDatei);
$handle = fopen($csvDatei, 'a');
if ($handle !== false) {
flock($handle, LOCK_EX);
if ($istNeu) {
fputcsv($handle, [
'Vorname', 'Nachname',
'E-Mail', 'Datum'
], ';');
}
fputcsv($handle, [
$vorname, $nachname,
$email, date('d.m.Y H:i')
], ';');
flock($handle, LOCK_UN);
fclose($handle);
$meldung = 'Daten gespeichert.';
} else {
$fehler[] = 'Datei nicht beschreibbar.';
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Kontaktformular</title>
</head>
<body>
<?php if ($meldung !== ''): ?>
<p style="color: green">
<?= htmlspecialchars($meldung) ?>
</p>
<?php endif; ?>
<?php if (count($fehler) > 0): ?>
<ul style="color: red">
<?php foreach ($fehler as $f): ?>
<li><?= htmlspecialchars($f) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<form action="" method="post">
<p>
<label>Vorname:
<input type="text" name="vorname"
required>
</label>
</p>
<p>
<label>Nachname:
<input type="text" name="nachname"
required>
</label>
</p>
<p>
<label>E-Mail:
<input type="email" name="email"
required>
</label>
</p>
<button type="submit">Absenden</button>
</form>
</body>
</html>
Gespeicherte Daten auslesen
Die CSV-Datei lässt sich mit fgetcsv() zeilenweise zurücklesen. So können die gespeicherten Einträge zum Beispiel in einer Tabelle angezeigt werden.
<?php
$csvDatei = __DIR__ . '/daten/kontakte.csv';
if (!file_exists($csvDatei)) {
echo '<p>Noch keine Einträge vorhanden.</p>';
} else {
$handle = fopen($csvDatei, 'r');
/* Kopfzeile lesen */
$header = fgetcsv($handle, 0, ';');
echo '<table border="1">';
echo '<tr>';
foreach ($header as $spalte) {
echo '<th>'
. htmlspecialchars($spalte)
. '</th>';
}
echo '</tr>';
while (($zeile = fgetcsv($handle, 0, ';'))
!== false
) {
echo '<tr>';
foreach ($zeile as $wert) {
echo '<td>'
. htmlspecialchars($wert)
. '</td>';
}
echo '</tr>';
}
echo '</table>';
fclose($handle);
}
?>
Die Kombination aus fputcsv() zum Schreiben und fgetcsv() zum Lesen stellt sicher, dass Sonderzeichen wie Semikolons oder Zeilenumbrüche in den Daten korrekt behandelt werden. Beim manuellen Zusammenbauen von CSV-Zeilen mit Punkt-Verkettung können solche Zeichen die Dateistruktur zerstören.
Sicherheitshinweise
Formulardaten kommen von außen und sind grundsätzlich nicht vertrauenswürdig. Neben der Eingabevalidierung gibt es weitere Punkte, die bei der Speicherung in Dateien zu beachten sind.
Datei außerhalb des Webroot speichern
Die CSV-Datei sollte nicht über den Browser abrufbar sein. Idealerweise liegt sie in einem Verzeichnis außerhalb des Document-Root. Falls das nicht möglich ist, hilft eine .htaccess-Datei im Daten-Verzeichnis.
# .htaccess im Verzeichnis /daten/
Deny from all
Dateiberechtigungen
Das Daten-Verzeichnis braucht Schreibrechte für den Webserver-Benutzer. Auf Linux-Servern reicht 0755 für das Verzeichnis und 0644 für die Dateien. Berechtigungen wie 0777 sind ein Sicherheitsrisiko und sollten vermieden werden.
Ausgabe immer mit htmlspecialchars()
Wenn die gespeicherten Daten später im Browser angezeigt werden, muss jeder Wert mit htmlspecialchars() behandelt werden. Sonst könnte ein Angreifer über das Formular HTML oder JavaScript einschleusen, das beim Auslesen der Daten ausgeführt wird.
Wann besser eine Datenbank verwenden?
Eine Textdatei eignet sich für kleine Datenmengen mit wenigen gleichzeitigen Zugriffen. Sobald Daten durchsucht, sortiert oder von mehreren Benutzern gleichzeitig geschrieben werden müssen, ist eine Datenbank wie MySQL oder SQLite die bessere Wahl. SQLite speichert die Daten ebenfalls in einer einzelnen Datei und lässt sich ohne zusätzlichen Server nutzen.