Abschnitt 11 · Lektion 3
Nach dem Absenden weiterleiten
Du hast ein Formular gebaut, das etwas entgegennimmt. Jetzt kommt der Ablauf drumherum, und der hat eine Falle, die man erst sieht, wenn man hineingetreten ist.
Was beim Neuladen passiert
<?php
// Nach dem Absenden antwortet diese Seite direkt mit dem Ergebnis.
// Schick das Formular ab und sieh in den Reiter Netzwerk: Die
// Seite vor dir ist die Antwort auf einen POST. Im Terminal
// schickt curl -s -X POST localhost:3000/ denselben POST noch
// einmal, und der Zaehler zaehlt weiter.
$datei = __DIR__ . "/bestellungen.txt";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
file_put_contents($datei, "eine Bestellung\n", FILE_APPEND);
}
$anzahl = file_exists($datei) ? count(file($datei)) : 0;
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Bestellen</title>
</head>
<body>
<h1>Bestellen ohne Weiterleitung</h1>
<form method="post" action="/">
<button type="submit">Jetzt bestellen</button>
</form>
<p>Bisher eingegangen: <strong><?= $anzahl ?></strong></p>
<p>Abgeschickt wurde diese Seite mit
<code><?= htmlspecialchars($_SERVER["REQUEST_METHOD"]) ?></code>.</p>
</body>
</html> Ein Browser merkt sich, wie er zu der Seite gekommen ist, die gerade vor ihm steht. Kam sie über einen Link, war das ein GET, und F5 heißt dann einfach: dieselbe Adresse noch einmal holen.
Kam sie aber als Antwort auf ein abgeschicktes Formular, war es ein POST, und dann bedeutet F5 etwas ganz anderes: dieselben Daten noch einmal hinschicken. Der Browser weiß, dass das heikel ist, und fragt vorher nach. Diese Nachfrage kennst du, und jetzt weißt du auch, warum sie kommt.
Das eigentliche Problem ist nicht die Nachfrage, sondern das, was ein Besucher tut: Er klickt sie weg. Danach steht die Bestellung zweimal in deiner Liste, und dein Programm hat nichts falsch gemacht, es hat zweimal genau das getan, was es sollte.
Die Nachfrage selbst bekommst du in der Lektionsansicht nicht zu sehen: F5 lädt hier die ganze
Lektion neu und nicht die Seite im Browser, und Seite neu laden holt die Adresse immer mit
einem GET. Was du sehen kannst, ist die Ursache und die Folge. Klick im Browser auf Jetzt
bestellen und sieh in den Reiter Netzwerk: Dort steht POST /, und die Seite vor dir ist die
Antwort darauf. Genau das merkt sich ein Browser. Und was er nach dem Wegklicken der Nachfrage
täte, nämlich denselben POST noch einmal schicken, tut im Terminal dieser Befehl:
curl -s -X POST localhost:3000/ | grep eingegangen. Zweimal ausgeführt, und der Zähler steht auf
zwei, ohne dass jemand ein Formular ausgefüllt hätte.
Das Muster dagegen
<?php
// Dieselbe Seite, drei Zeilen anders. Nach dem Speichern geht eine
// Weiterleitung hinaus, und der Browser holt die Zielseite mit
// einem ganz gewoehnlichen GET. Im Reiter Netzwerk sind das zwei
// Zeilen statt einer.
$datei = __DIR__ . "/bestellungen.txt";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
file_put_contents($datei, "eine Bestellung\n", FILE_APPEND);
header("Location: /?bestellt=1", true, 302);
exit;
}
$anzahl = file_exists($datei) ? count(file($datei)) : 0;
$eben = isset($_GET["bestellt"]);
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Bestellen</title>
</head>
<body>
<h1>Bestellen mit Weiterleitung</h1>
<?php if ($eben) { ?>
<p><strong>Danke, deine Bestellung ist angekommen.</strong></p>
<?php } ?>
<form method="post" action="/">
<button type="submit">Jetzt bestellen</button>
</form>
<p>Bisher eingegangen: <strong><?= $anzahl ?></strong></p>
<p>Abgeschickt wurde diese Seite mit
<code><?= htmlspecialchars($_SERVER["REQUEST_METHOD"]) ?></code>.</p>
</body>
</html> Die Lösung ist so alt wie das Problem und heißt „nach dem Speichern weiterleiten”. Der Ablauf hat drei Schritte statt zwei:
Der POST kommt an, du erledigst deine Arbeit, und dann antwortest du nicht mit einer Seite,
sondern mit einer Weiterleitung. Der Browser sieht die Kopfzeile Location, holt die genannte
Adresse mit einem gewöhnlichen GET, und diese zweite Antwort ist die Seite, die der Besucher sieht.
Damit steht am Ende ein GET vor ihm, und Neuladen heißt wieder das Harmlose. Der POST ist
Vergangenheit. Im Reiter Netzwerk siehst du den Ablauf nach dem Klick als zwei Zeilen: erst
POST / mit dem Statuscode 302 und Location: /?bestellt=1 in den Antwort-Kopfzeilen, darunter
GET /?bestellt=1 mit 200. Die Adresszeile zeigt die zweite, und die Seite sagt es auch:
Abgeschickt wurde sie mit GET.
Die Kopfzeile hast du in Lektion 10.4 gesetzt, und hier ist sie wieder:
Statuscode 302 heißt „was du suchst, liegt woanders”. Es gibt auch 301, das heißt „liegt dauerhaft woanders”, und das ist hier falsch: Browser merken sich eine 301 und rufen die alte Adresse gar nicht mehr auf. Für ein Formular willst du 302.
Und wie in 10.4: Die Kopfzeile muss hinaus, bevor das erste Zeichen der Seite unterwegs ist. Der
header()-Aufruf gehört deshalb in den PHP-Block ganz oben, nicht irgendwo zwischen das Markup.
Warum das exit dazugehört
<?php
// Hier fehlt das exit mit Absicht. Nach header() ist das Programm
// eben nicht zu Ende, und die Zeile darunter schreibt munter
// weiter. Ein Klick auf den Knopf, zwei Eintraege im Protokoll.
$datei = __DIR__ . "/protokoll.txt";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
file_put_contents($datei, "gespeichert\n", FILE_APPEND);
header("Location: /?fertig=1", true, 302);
// Ohne exit laeuft PHP hier weiter.
file_put_contents($datei, "und aus Versehen noch einmal gespeichert\n", FILE_APPEND);
}
$zeilen = file_exists($datei) ? file($datei, FILE_IGNORE_NEW_LINES) : [];
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Protokoll</title>
</head>
<body>
<h1>Ein Klick, zwei Einträge</h1>
<form method="post" action="/">
<button type="submit">Einmal speichern</button>
</form>
<ol>
<?php foreach ($zeilen as $zeile) { ?>
<li><?= htmlspecialchars($zeile) ?></li>
<?php } ?>
</ol>
</body>
</html> header() verschickt nichts, es notiert nur. Und vor allem: Es beendet dein Programm nicht. Die
Zeilen darunter laufen weiter, als wäre nichts gewesen.
Beim Ausgeben von Markup fällt das kaum auf, denn der Browser wirft den Rumpf einer Weiterleitung
sowieso weg. Im Terminal kannst du ihn trotzdem sehen: curl -si -X POST localhost:3000/ zeigt
beim dritten Beispiel die Statuszeile mit der 302, darunter Location: /?fertig=1, und darunter
die ganze Seite, die kein Browser je anzeigt. Bei allem anderen fällt es sehr wohl auf: Eine Zeile,
die noch etwas speichert, speichert. Eine Zeile, die eine Mail verschickt, verschickt. Der Besucher
sieht davon nichts, und genau deshalb sucht man den Fehler tagelang an der falschen Stelle.
Deshalb steht hinter jeder Weiterleitung ein exit. Ohne Ausnahme, auch wenn darunter gerade nichts
Wichtiges steht: Was heute harmlos ist, ist nach dem nächsten Umbau die Zeile, die doppelt bucht.
Im dritten Beispiel fehlt das exit. Klick einmal auf den Knopf und zähl die Einträge.
Wie die Meldung ans Ziel kommt
Nach der Weiterleitung fängt ein neuer Lauf an, und der weiß nichts von dem, was eben passiert ist. Alle Variablen sind weg, das ist die Eigenschaft aus Lektion 10.1, die dich durch den ganzen Kurs begleitet.
Für heute reicht der einfachste Weg: Du hängst die Information an die Adresse, also
Location: /?gespeichert=1, und die Zielseite liest sie aus $_GET. Das ist ehrlich, sofort
verständlich und hat genau einen Nachteil: Es steht in der Adresszeile, und wer sie kopiert und
weitergibt, verschickt eine Bestätigung für etwas, das nie passiert ist.
Der saubere Weg legt die Meldung stattdessen beim Server ab und holt sie am Ziel genau einmal wieder heraus. Dafür braucht es eine Sitzung, und die ist der Stoff von Lektion 12.4.
Zum Mitnehmen
Fast jedes selbst gebaute Formular hat am Anfang denselben Fehler: Man drückt F5 und bestellt aus Versehen ein zweites Mal. Dagegen hilft ein Dreizeiler, den man einmal gesehen haben muss. Diese Lektion solltest du wirklich anfassen, im Browser, im Reiter Netzwerk und im Terminal, denn der Unterschied ist ein Erlebnis und kein Sachverhalt.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.