# PHP-Backend ohne Framework

## Inhaltsverzeichnis

- [Page Controller statt MVC](#page-controller-statt-mvc)
- [Verbindung zur Datenbank mit PDO](#verbindung-zur-datenbank-mit-pdo)
- [Prepared Statements gegen SQL-Injection](#prepared-statements-gegen-sql-injection)
- [Passwörter hashen statt speichern](#passwörter-hashen-statt-speichern)
- [Login mit Sessions](#login-mit-sessions)
- [Suche und Filter im selben Query bauen](#suche-und-filter-im-selben-query-bauen)
- [Transaktionen](#transaktionen)
- [Beispielaufgabe](#beispielaufgabe)
- [Zusammenfassung / Spickzettel](#zusammenfassung--spickzettel)

---

## Page Controller statt MVC

Im Unterricht ging es um den Model-View-Controller: ein Model für die Daten, ein Controller, der entscheidet was passiert, eine View, die es anzeigt, meist mit einem zentralen Router davor, der jede Anfrage zuerst entgegennimmt. Für ein Projekt in dieser Grösse (eine Handvoll Seiten, kein Team dahinter) habe ich mich bewusst dagegen entschieden und stattdessen mit **Page Controllern** gearbeitet: eine PHP-Datei entspricht direkt einer URL, kümmert sich selbst um ihre Logik und gibt am Ende ihr eigenes HTML aus. Kein Router, kein Framework, keine Konfigurationsdatei, die zwischen URL und Code vermittelt.

```
antiquariat/
├── index.php        → Startseite
├── books.php        → Bücherliste, Suche, Filter
├── book.php         → einzelnes Buch (?id=…)
├── login.php        → Admin-Login
├── admin/
│   └── books.php    → Bücher verwalten (nur eingeloggt)
└── includes/
    ├── db.php        → eine Funktion, die eine PDO-Verbindung zurückgibt
    ├── auth.php       → isLoggedIn(), requireLogin()
    ├── header.php / footer.php
```

Jede Datei bindet sich das ein, was sie braucht (`require_once __DIR__ . '/includes/db.php';`), und ist danach für sich lauffähig. Der Nachteil: Logik, die auf mehreren Seiten vorkommt (etwa der Login-Check), muss ich selbst konsequent in jede Datei einbauen, ein Router würde das erzwingen. Der Vorteil, der für mich den Ausschlag gab: Ich konnte an jeder Seite einzeln arbeiten und genau sehen, was beim Aufruf einer URL passiert, ohne durch mehrere Schichten zu springen.

## Verbindung zur Datenbank mit PDO

PHP kennt mehrere Wege, mit MySQL zu sprechen (die alte `mysql_*`-Familie, `mysqli`, `PDO`). Ich habe **PDO** genommen, weil dieselbe Klasse mit jeder unterstützten Datenbank funktioniert und die Fehlerbehandlung über Exceptions läuft statt über Rückgabewerte, die man vergessen kann zu prüfen.

```php
function getDb(): PDO {
    static $verbindung = null;
    if ($verbindung !== null) return $verbindung;

    $verbindung = new PDO(
        'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8',
        DB_USER,
        DB_PASS,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    return $verbindung;
}
```

Das `static` sorgt dafür, dass innerhalb eines Seitenaufrufs nur eine Verbindung aufgebaut wird, egal wie oft `getDb()` gerufen wird.

## Prepared Statements gegen SQL-Injection

Der wichtigste Sicherheitspunkt im ganzen Modul. Eine Query, die Benutzereingaben direkt in den String hängt, lässt sich manipulieren:

```php
// So NICHT:
$db->query("SELECT * FROM buecher WHERE titel LIKE '%$suche%'");
```

Gibt jemand als Suchbegriff `%' OR '1'='1` ein, wird aus der harmlosen Suche eine Bedingung, die immer wahr ist, und die ganze Tabelle liegt offen. Ein **Prepared Statement** trennt die Anweisung von den Werten: Platzhalter (`?`) stehen im SQL-Text, die eigentlichen Werte gehen als Parameter mit, und der Treiber verhindert, dass darin enthaltenes SQL ausgeführt wird.

```php
$stmt = $db->prepare('SELECT * FROM buecher WHERE titel LIKE ?');
$stmt->execute(["%$suche%"]);
$treffer = $stmt->fetchAll();
```

Bei mehreren Bedingungen baue ich mir die Fragezeichen und die Werte parallel in zwei Arrays auf und übergebe sie zusammen an `execute()`, statt irgendwo mitten im Query-String einen Wert einzusetzen.

## Passwörter hashen statt speichern

Ein Passwort im Klartext in der Datenbank ist auch bei zugriffsgeschützter Datenbank ein Risiko: Backups, ein Leck, ein Mitarbeiter mit Zugriff, alles genügt. PHP bringt dafür `password_hash()` und `password_verify()` mit, beide arbeiten mit bcrypt.

```php
// Beim Anlegen des Kontos
$hash = password_hash($eingabe, PASSWORD_DEFAULT);
// $hash sieht z.B. so aus: $2y$10$N9qo8uLOickgx2ZMRZoMye...

// Beim Login
if (password_verify($eingabe, $hashAusDerDatenbank)) {
    // Passwort stimmt
}
```

Der Hash enthält Algorithmus, Kostenfaktor und ein zufälliges Salt in einem String, `password_verify()` liest das selbst wieder heraus. Ich muss also nirgends selbst ein Salt verwalten oder vergleichen, nur den fertigen Hash speichern und beim Login durchreichen.

## Login mit Sessions

Eine Anmeldung muss über mehrere Seitenaufrufe hinweg bekannt bleiben, HTTP selbst kennt aber keinen Zustand zwischen zwei Requests. Dafür gibt es Sessions: der Server merkt sich Daten unter einer ID, die er dem Browser als Cookie mitgibt, und jede folgende Anfrage bringt dieses Cookie automatisch mit.

```php
session_start();               // an den Anfang jeder Datei, die die Session braucht

// nach erfolgreichem Login:
$_SESSION['benutzer'] = ['id' => $zeile['id'], 'name' => $zeile['name']];

// Zugriffsschutz auf einer Admin-Seite:
if (empty($_SESSION['benutzer'])) {
    header('Location: /login.php');
    exit;
}
```

Wichtig: `exit;` direkt nach dem `header()`-Aufruf, sonst läuft der Rest des Skripts trotz Weiterleitung weiter und könnte noch Daten ausgeben, bevor der Browser überhaupt umschaltet.

## Suche und Filter im selben Query bauen

Auf der Bücherliste kommen mehrere Bedingungen gleichzeitig vor: ein Suchbegriff, eine Kategorie, ein Zustand. Statt für jede Kombination eine eigene Query zu schreiben, sammle ich die Bedingungen und die passenden Werte in zwei Arrays und setze sie erst am Ende zusammen.

```php
$bedingungen = ['1=1'];
$werte = [];

if ($suche !== '') {
    $bedingungen[] = 'titel LIKE ?';
    $werte[] = "%$suche%";
}
if ($kategorieId > 0) {
    $bedingungen[] = 'kategorie_id = ?';
    $werte[] = $kategorieId;
}

$sql = 'SELECT * FROM buecher WHERE ' . implode(' AND ', $bedingungen);
$stmt = $db->prepare($sql);
$stmt->execute($werte);
```

Die `1=1` am Anfang ist ein kleiner Trick: so kann jede weitere Bedingung einfach mit `AND` angehängt werden, ohne vorher prüfen zu müssen, ob es schon eine gibt.

## Transaktionen

Wenn eine Aktion aus mehreren zusammengehörigen SQL-Befehlen besteht (zum Beispiel: einen Verkauf eintragen UND den Lagerbestand verringern), sollen entweder beide durchlaufen oder keiner. Eine Transaktion fasst mehrere Anweisungen zu einer Einheit zusammen:

```php
$db->beginTransaction();
try {
    $db->prepare('UPDATE buecher SET verkauft = 1 WHERE id = ?')->execute([$id]);
    $db->prepare('INSERT INTO verkaeufe (buch_id) VALUES (?)')->execute([$id]);
    $db->commit();
} catch (Exception $e) {
    $db->rollBack();
}
```

Bricht der zweite Befehl ab, macht `rollBack()` auch den ersten wieder rückgängig, die Datenbank bleibt konsistent.

## Beispielaufgabe

Eine typische Übung aus dem Modul: eine Funktion schreiben, die einen Suchbegriff und eine Tabelle entgegennimmt und sicher (also mit Prepared Statement) alle passenden Zeilen zurückgibt, ohne dass ein Suchbegriff wie `'; DROP TABLE buecher; --` irgendeinen Schaden anrichten kann. Genau das testet man, indem man absichtlich mit solchen Zeichen sucht und prüft, dass nichts weiter passiert als eine leere Trefferliste.

## Zusammenfassung / Spickzettel

| Thema | Kernaussage |
|---|---|
| Architektur | Page Controller: eine Datei = eine URL, kein Framework dazwischen |
| Datenbankzugriff | PDO, eine geteilte Verbindung pro Request |
| Eingaben | Nie in den SQL-String einsetzen, immer als Parameter mitgeben |
| Passwörter | `password_hash()` / `password_verify()`, nie im Klartext |
| Zustand über Requests | `$_SESSION`, geschützt hinter `session_start()` |
| Mehrere zusammengehörige Schreibvorgänge | Transaktion mit `commit()` / `rollBack()` |
