mitmario.dev

Eine Liste, die wächst

PHP Sandbox 3 Min Lesezeit 3 BeispieleLektion 5 von 7

Diese Lektion baut nichts Neues, sie setzt zusammen. Ein Formular aus 11.1, eine Prüfung aus 11.2, eine Weiterleitung aus 11.3, eine entschärfte Ausgabe aus 11.4 und eine JSON-Datei aus Abschnitt 8.

Der Ablauf, einmal am Stück

Die Liste in einer Datei
<?php

// Alles aus diesem Abschnitt zusammen: entgegennehmen, pruefen,
// anhaengen, weiterleiten, anzeigen. Leg im Browser drei, vier Notizen
// an und sieh der Liste beim Wachsen zu.

function e(?string $wert): string
{
    return htmlspecialchars($wert ?? "", ENT_QUOTES, "UTF-8");
}

$datei = __DIR__ . "/notizen.json";
$notizen = json_decode(file_get_contents($datei), true) ?? [];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $text = trim($_POST["text"] ?? "");

    if ($text !== "") {
        $notizen[] = $text;
        file_put_contents($datei, json_encode($notizen, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    header("Location: /", true, 302);
    exit;
}

?>
<!doctype html>
<html lang="de">
<head>
    <meta charset="utf-8">
    <title>Notizen</title>
</head>
<body>
    <h1>Notizen</h1>

    <form method="post" action="/">
        <p><label>Neue Notiz: <input type="text" name="text" size="30"></label></p>
        <button type="submit">Anlegen</button>
    </form>

    <ul>
<?php foreach ($notizen as $notiz) { ?>
        <li><?= e($notiz) ?></li>
<?php } ?>
    </ul>

    <p>Einträge: <?= count($notizen) ?></p>
</body>
</html>

Fünf Schritte, immer in dieser Reihenfolge:

Zuerst wird die Liste gelesen, ganz oben und in jedem Fall. Sie wird sowohl beim Anzeigen gebraucht als auch beim Anhängen, und einmal lesen ist besser als zweimal.

Dann kommt die Frage, ob überhaupt etwas abgeschickt wurde. Wenn ja: prüfen, ob der Text brauchbar ist, ihn hinten anhängen, die Datei zurückschreiben und weiterleiten.

Und darunter die Anzeige, die nur noch ausgibt, was in der Liste steht, jeden Eintrag durch e().

Bemerkenswert ist, was hier nicht steht: kein zweiter Zweig für das Anzeigen. Nach dem Weiterleiten ist das Programm zu Ende, alles darunter gilt also nur noch für ein GET. Das spart eine Verschachtelung, und es ist der übliche Aufbau einer solchen Datei.

Leg im Browser ein paar Notizen an. Nach jedem Absenden wechselt die Adresszeile zurück auf /, und im Reiter Netzwerk stehen je Notiz zwei Zeilen: der POST mit seiner 302 und das GET, das die Liste holt. Die Seite vor dir ist immer die zweite.

Die Datei gibt es noch nicht

Wenn die Datei noch nicht da ist
<?php

// Beim allerersten Aufruf einer frisch hochgeladenen Anwendung
// gibt es die Datei noch gar nicht. Das ist der Normalfall und
// kein Fehler, behandelt werden muss er trotzdem.

$datei = __DIR__ . "/notizen.json";

echo "So nicht:\n";
$roh = file_get_contents($datei);
var_dump($roh);

echo "\nSo schon:\n";
$notizen = is_file($datei) ? (json_decode(file_get_contents($datei), true) ?? []) : [];
var_dump($notizen);

echo "\nUnd nach dem ersten Schreiben:\n";
file_put_contents($datei, json_encode(["Milch"]));
var_dump(json_decode(file_get_contents($datei), true));

Beim ersten Aufruf einer frisch hochgeladenen Anwendung ist die Datei nicht da. file_get_contents schreibt dann eine Warnung auf den Fehlerkanal und gibt false zurück, und json_decode(false) liefert null. Das Programm läuft weiter und arbeitet ab da mit einem Wert, den niemand gemeint hat.

Zwei Handgriffe machen daraus einen sauberen Fall. is_file() fragt vorher, ob es die Datei überhaupt gibt, und ?? [] fängt ab, dass in der Datei etwas Kaputtes stand. Zusammen heißt das: Wenn nichts da ist, fang mit einer leeren Liste an.

Dieses Beispiel läuft als Skript und nicht als Server, mit php index.php im Terminal, und das hat einen Grund: Dort stehen Ausgabe und Warnung auf zwei Kanälen nebeneinander, die Warnung rot, und man sieht auf einen Blick, welche der drei Fassungen sie ausgelöst hat. Auf einer Seite stünde sie mitten im Text. Das dritte Beispiel läuft aus demselben Grund als Skript: Es zeigt eine Reihenfolge, keine Seite.

Warum das noch keine Datenbank ist

Zwei Besucher im selben Moment
<?php

// Zwei Besucher kommen gleichzeitig an. Beide lesen dieselbe
// Liste, beide haengen etwas an, beide schreiben zurueck. Einer
// der beiden Eintraege ist danach weg.

$datei = __DIR__ . "/liste.json";
file_put_contents($datei, json_encode(["Brot"]));

// Besucher A liest.
$a = json_decode(file_get_contents($datei), true);

// Besucher B liest im selben Moment und sieht dieselbe Liste.
$b = json_decode(file_get_contents($datei), true);

// A haengt an und schreibt zurueck.
$a[] = "Milch";
file_put_contents($datei, json_encode($a));

// B haengt an und schreibt zurueck. Seine Liste kennt die Milch
// nicht, denn er hat vor A gelesen.
$b[] = "Butter";
file_put_contents($datei, json_encode($b));

echo "In der Datei steht jetzt: ", file_get_contents($datei), "\n";
echo "Die Milch von Besucher A ist weg, und keiner der beiden hat\n";
echo "etwas falsch gemacht.\n";

Eine JSON-Datei als Speicher ist für eine kleine Sache völlig in Ordnung, und viele Werkzeuge, die du benutzt, machen es nicht anders. Eine Grenze hat sie trotzdem, und die liegt näher, als man denkt.

Der Ablauf ist ja: lesen, ändern, zurückschreiben. Zwischen dem Lesen und dem Zurückschreiben vergeht Zeit, und in dieser Zeit kann ein zweiter Besucher dieselbe Datei gelesen haben. Er schreibt dann eine Liste zurück, in der die Änderung des ersten nie vorkam. Ein Eintrag ist weg, niemand hat einen Fehler gemacht, und in keinem Protokoll steht etwas davon.

Bei zwei Notizen am Tag passiert das nie. Bei zwanzig Besuchern gleichzeitig passiert es, und zwar nicht jedes Mal, sondern manchmal. Das ist die unangenehmste Sorte Fehler.

Es gibt Mittel dagegen. flock() sperrt eine Datei, solange man an ihr arbeitet, und file_put_contents kennt dafür das Flag LOCK_EX. Damit kommt man ein gutes Stück weiter, und für viele kleine Anwendungen reicht es.

Der eigentliche Ausweg ist ein Programm, das genau dafür gebaut ist: eine Datenbank. Sie nimmt mehrere Schreibende gleichzeitig entgegen, sie kann in einem Zug ändern statt zu lesen und zurückzuschreiben, und sie findet einen Eintrag, ohne die ganze Liste durchzugehen. In Abschnitt 14 baust du dieselbe Notizliste noch einmal, dann mit SQLite, und dann verschwindet dieser Absatz.

Zum Mitnehmen

Alles aus diesem Abschnitt an einer Stelle: Ein Formular nimmt etwas entgegen, eine Prüfung sieht es sich an, eine Datei behält es, und die Seite zeigt es wieder. Damit hast du zum ersten Mal etwas gebaut, das über einen Aufruf hinaus etwas behält. Und dabei sieht man auch, wo eine Datei aufhört zu genügen.

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.