Abschnitt 14 · Lektion 8
Die Liste mit Datenbank
Die Notizliste aus Lektion 11.5 lag in einer JSON-Datei. Alles, was du seither gelernt hast, reicht aus, um sie auf eine Datenbank umzustellen: verbinden, Tabelle anlegen, einfügen mit Platzhalter, abfragen. Neuen Stoff gibt es in dieser Lektion keinen mehr.
Die Frage ist eine andere, und sie taucht bei jedem Projekt an genau dieser Stelle auf.
Alles in einer Datei
<?php
// Alles in einer Datei: Verbindung, Tabelle, Abfrage, Formular,
// Ausgabe. So faengt fast jedes kleine Projekt an.
$db = new PDO("sqlite:" . __DIR__ . "/daten.db", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec("CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL
)");
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$text = trim($_POST["text"] ?? "");
if ($text !== "") {
$einfuegen = $db->prepare("INSERT INTO notizen (titel) VALUES (:titel)");
$einfuegen->execute([":titel" => $text]);
}
header("Location: /", true, 302);
exit;
}
$notizen = $db->query("SELECT id, titel FROM notizen ORDER BY id DESC")->fetchAll();
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Notizen, alles in einer Datei</title>
<style>body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; }</style>
</head>
<body>
<h1>Notizen</h1>
<p>Diese Seite macht alles selbst. Zaehl mal, wie viele Dinge das sind.</p>
<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>#<?= $notiz["id"] ?> <?= htmlspecialchars($notiz["titel"], ENT_QUOTES, "UTF-8") ?></li>
<?php } ?>
</ul>
</body>
</html> So sieht es aus, wenn man einfach anfängt, und es ist völlig in Ordnung. Die Seite läuft, du kannst im Reiter Browser Notizen anlegen, und für zwanzig Zeilen ist das die schnellste Lösung.
Zähl trotzdem einmal durch, was diese eine Datei alles weiß. Sie kennt den Pfad zur Datenbankdatei.
Sie kennt die Spalten der Tabelle. Sie kennt zwei SQL-Anweisungen. Sie liest $_POST, sie schickt
Kopfzeilen, und sie erzeugt HTML. Das sind sechs verschiedene Zuständigkeiten in einer Datei.
Solange es bei einer Datei bleibt, spürst du davon nichts. Der Ärger fängt bei der zweiten an. Eine
Seite erledigt.php, die nur die abgehakten Notizen zeigt, braucht dieselbe Verbindung und dasselbe
CREATE TABLE. Also kopierst du. Und beim nächsten Mal, wenn eine Spalte dazukommt, änderst du an
zwei Stellen, oder eben an einer, und die zweite geht kaputt.
Dasselbe mit einer Klasse dazwischen
<?php
// Alles, was mit Speichern zu tun hat, und sonst nichts. Kein Markup,
// kein $_POST, kein header().
class Notizspeicher
{
private PDO $db;
public function __construct(string $datei)
{
$this->db = new PDO("sqlite:" . $datei, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->db->exec("CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL
)");
}
public function alle(): array
{
return $this->db->query("SELECT id, titel FROM notizen ORDER BY id DESC")->fetchAll();
}
public function anlegen(string $titel): int
{
$einfuegen = $this->db->prepare("INSERT INTO notizen (titel) VALUES (:titel)");
$einfuegen->execute([":titel" => $titel]);
return (int) $this->db->lastInsertId();
}
} <?php
// Dieselbe Seite. Sie kennt PDO nicht mehr und kein SQL.
require __DIR__ . "/Notizspeicher.php";
$speicher = new Notizspeicher(__DIR__ . "/daten.db");
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$text = trim($_POST["text"] ?? "");
if ($text !== "") {
$speicher->anlegen($text);
}
header("Location: /", true, 302);
exit;
}
$notizen = $speicher->alle();
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Notizen mit Notizspeicher</title>
<style>body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; }</style>
</head>
<body>
<h1>Notizen</h1>
<p>Dieselbe Seite, nur weiss sie nicht mehr, wo die Notizen liegen.</p>
<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>#<?= $notiz["id"] ?> <?= htmlspecialchars($notiz["titel"], ENT_QUOTES, "UTF-8") ?></li>
<?php } ?>
</ul>
</body>
</html> Die Seite ist danach kürzer, und das ist der kleinere Teil des Gewinns. Der größere ist, was sie
nicht mehr weiß: Sie kennt weder PDO noch SQL noch die Spaltennamen. Sie sagt anlegen($text)
und alle(), und wo das landet, ist nicht ihr Problem.
Das ist dieselbe Idee wie in Lektion 13.4, nur diesmal an etwas Echtem: Eine Klasse bündelt Daten und die Dinge, die man damit tut. Hier bündelt sie die Verbindung und alles, was mit ihr passiert.
Die Aufteilung folgt einer einfachen Frage: Ändert sich das, weil die Anzeige anders wird, oder weil die Speicherung anders wird? Das Markup gehört zur Anzeige, das SQL zur Speicherung, und die beiden ändern sich zu verschiedenen Zeiten und aus verschiedenen Gründen. Also gehören sie in verschiedene Dateien.
Ein Nebeneffekt, den man erst später zu schätzen weiß: Willst du irgendwann von SQLite auf MySQL wechseln, ist das jetzt eine Änderung an einer Datei. In der Fassung davor wäre es eine Suche über das ganze Projekt.
<?php
// Alles, was mit Speichern zu tun hat, und sonst nichts. Kein Markup,
// kein $_POST, kein header().
class Notizspeicher
{
private PDO $db;
public function __construct(string $datei)
{
$this->db = new PDO("sqlite:" . $datei, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$this->db->exec("CREATE TABLE IF NOT EXISTS notizen (
id INTEGER PRIMARY KEY,
titel TEXT NOT NULL
)");
}
public function alle(): array
{
return $this->db->query("SELECT id, titel FROM notizen ORDER BY id DESC")->fetchAll();
}
public function anlegen(string $titel): int
{
$einfuegen = $this->db->prepare("INSERT INTO notizen (titel) VALUES (:titel)");
$einfuegen->execute([":titel" => $titel]);
return (int) $this->db->lastInsertId();
}
} <?php
// Dieselbe Klasse, ohne Webserver. Ein Skript fuer die Kommandozeile,
// das sich um Formulare und Markup nicht kuemmert.
require __DIR__ . "/Notizspeicher.php";
$speicher = new Notizspeicher(__DIR__ . "/daten.db");
echo "Neu angelegt als Nummer ", $speicher->anlegen("Aus dem Skript"), "\n\n";
echo "Alle Notizen:\n";
foreach ($speicher->alle() as $notiz) {
echo " #", $notiz["id"], " ", $notiz["titel"], "\n";
}
echo "\nInsgesamt ", count($speicher->alle()), " Stueck.\n"; Und hier ist der Beweis, dass die Trennung echt ist und nicht nur ordentlich aussieht: Dasselbe
Notizspeicher.php, aber diesmal benutzt es ein Kommandozeilenskript. Kein Formular, kein HTML,
kein Webserver, und trotzdem ändert sich an der Klasse kein Zeichen. Genau so schreibt man später
einen Import, eine Auswertung oder einen nächtlichen Aufräumjob. Lass es im Terminal mit
php bericht.php ein zweites Mal laufen: Jetzt steht dort Nummer 2 und 2 Stueck, denn die
Datenbankdatei bleibt zwischen zwei Läufen liegen, wie sie es bei einem nächtlichen Job auch täte.
Was diese Klasse nicht ist
Sie ist kein Framework und kein „Repository Pattern”. Sie ist eine Klasse mit einer Verbindung und drei Methoden, und mehr braucht sie für diese Anwendung nicht.
Zwei Dinge fehlen ihr trotzdem, und du sollst wissen, dass sie fehlen. Erstens öffnet sie bei jeder
Anfrage eine neue Verbindung und ruft CREATE TABLE IF NOT EXISTS auf. Das ist bei SQLite billig
und bei einem Datenbankserver nicht mehr; dort legt man das Schema einmal an und nicht bei jedem
Aufruf. Zweitens gibt sie einfache Arrays zurück und keine Objekte. Für eine Notiz mit zwei Spalten
ist das genau richtig, und ab einer gewissen Größe wird daraus eine Klasse Notiz.
Beides sind Entscheidungen und keine Versäumnisse. Der Punkt dieser Lektion ist nicht die perfekte Aufteilung, sondern die erste: Speichern hierhin, Anzeigen dorthin. Abschnitt 16 nimmt sich vor, was danach kommt.
Zum Mitnehmen
Nichts Neues mehr, nur eine Frage: Wohin gehört das SQL? Nicht in die Seite, und der Grund dafür ist nicht Schönheit, sondern dass du dieselbe Sache sonst an fünf Stellen pflegst.
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.