Kapitel 13
checkout.php – Bestellung & Rechnung
Minimal-Lösung: eine Seite, die den Warenkorb als druckbare Rechnung anzeigt und in die DB einträgt. Statt eines echten Zahlungsmoduls wird die Rechnung mit Strg+P ausgedruckt (oder als PDF gespeichert).
Der Code
checkout.php
<?php
require_once('db.php');
session_start();
// login pflicht
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$warenkorb = $_SESSION['warenkorb'] ?? [];
if (empty($warenkorb)) {
header('Location: warenkorb.php');
exit;
}
// produktdaten holen (gleiche technik wie warenkorb.php)
$ids = array_keys($warenkorb);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $conn->prepare("SELECT id, name, price FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$produkte = $stmt->fetchAll(PDO::FETCH_ASSOC);
$gesamt = 0;
foreach ($produkte as $p) {
$gesamt += $p['price'] * $warenkorb[$p['id']];
}
// bestellung in db speichern (nur beim ersten aufruf!)
if (!isset($_SESSION['letzte_bestellung_id'])) {
$conn->beginTransaction();
try {
// bestellung anlegen
$stmt = $conn->prepare("INSERT INTO orders (user_id) VALUES (?)");
$stmt->execute([$_SESSION['user_id']]);
$bestell_id = $conn->lastInsertId();
// positionen anlegen
$pos = $conn->prepare("INSERT INTO order_items (order_id, product_id, menge, preis) VALUES (?, ?, ?, ?)");
foreach ($produkte as $p) {
$pos->execute([$bestell_id, $p['id'], $warenkorb[$p['id']], $p['price']]);
}
$conn->commit();
$_SESSION['letzte_bestellung_id'] = $bestell_id;
unset($_SESSION['warenkorb']);
} catch (Exception $e) {
$conn->rollBack();
die('Bestellung fehlgeschlagen.');
}
}
$bestell_id = $_SESSION['letzte_bestellung_id'];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Rechnung Nr. <?= (int)$bestell_id ?></title>
<link rel="stylesheet" href="styles.css">
<style>
/* nur beim drucken: navbar und buttons ausblenden */
@media print {
ul, .no-print { display: none; }
}
</style>
</head>
<body>
<?php include('navbar.php'); ?>
<h1>Rechnung Nr. <?= (int)$bestell_id ?></h1>
<p>Kunde: <?= htmlspecialchars($_SESSION['username'], ENT_QUOTES, 'UTF-8') ?></p>
<p>Datum: <?= date('d.m.Y H:i') ?></p>
<table border="1" cellpadding="8">
<tr><th>Produkt</th><th>Menge</th><th>Einzelpreis</th><th>Summe</th></tr>
<?php foreach ($produkte as $p): $m = $warenkorb[$p['id']] ?? 1; ?>
<tr>
<td><?= htmlspecialchars($p['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= (int)$m ?></td>
<td><?= number_format($p['price'], 2, ',', '.') ?> €</td>
<td><?= number_format($p['price'] * $m, 2, ',', '.') ?> €</td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="3"><strong>Gesamt:</strong></td>
<td><strong><?= number_format($gesamt, 2, ',', '.') ?> €</strong></td>
</tr>
</table>
<br>
<div class="no-print">
<button onclick="window.print()">Rechnung drucken (Strg+P)</button>
<a href="index.php">Zurück zum Shop</a>
</div>
</body>
</html>
Was hier passiert
- Login-Pflicht – bestellen ohne Account geht nicht.
- Warenkorb leer? → Redirect – keine Rechnung über nichts.
- Transaction (beginTransaction / commit / rollBack) – die Bestellung und ihre Positionen sind eine logische Einheit. Wenn beim Einfügen einer Position was schiefgeht, wird die halbfertige Bestellung wieder zurückgerollt. Atomicity.
- Session-Merker
letzte_bestellung_id– verhindert doppelte Bestellungen bei F5 auf der Rechnungsseite. Ist die Bestellung einmal eingetragen, wird beim erneuten Aufruf nur noch angezeigt. - Warenkorb leeren –
unset($_SESSION['warenkorb'])nach erfolgreicher Bestellung. - @media print – CSS-Regel, die beim Drucken die navbar und die Buttons ausblendet. So sieht die gedruckte Seite nach echter Rechnung aus.
Prüfer-Frage garantiert: "Was ist eine Transaktion und warum brauchst du sie hier?" → Weil eine Bestellung ohne Positionen sinnlos ist. Entweder werden beide Inserts durchgeführt oder keiner. Rollback bei Fehlern.
Für Voll-DSGVO-konform: zusätzliche Felder wie Rechnungs- und Lieferadresse in einer Tabelle
rechnungen speichern und beim Bestellen abfragen. Für die Minimal-Version reicht der Username. Wenn der Prüfer nach Rechnungsadresse fragt: sag, dass sie beim Registrieren erfasst wird – dann fehlt in users noch eine Adress-Spalte.