mitmario.dev

Nach dem Absenden weiterleiten

PHP Sandbox 4 Min Lesezeit 3 BeispieleLektion 3 von 7

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

Ohne Weiterleitung steht am Ende ein POST
<?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

Mit Weiterleitung steht am Ende ein GET
<?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

Was ohne exit noch weiterläuft
<?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, kostenlos

Zu 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.