Kodokon kodokon.com

PDO: Prepared Statements und Transaktionen

Konfiguriere PDO so, wie es in Produktion gemacht wird, schließe SQL-Injection strukturell aus und mache deine Schreibvorgänge mit Transaktionen zuverlässig.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

PDO ist die einheitliche Schnittstelle für den Datenbankzugriff: MySQL, PostgreSQL oder SQLite werden alle mit derselben API gesteuert. Drei Einstellungen trennen eine naive Verbindung von einer produktionsreifen: ERRMODE_EXCEPTION, damit jeder SQL-Fehler eine Exception auslöst, statt stillschweigend zu scheitern, FETCH_ASSOC als Standard für saubere Ergebnisse und EMULATE_PREPARES auf false für Prepared Statements, die tatsächlich vom Server ausgeführt werden. Füge charset=utf8mb4 direkt im DSN hinzu - das ist der einzige zuverlässige Ort, um es zu deklarieren.

PHP
<?php

declare(strict_types=1);

$dsn = 'mysql:host=localhost;dbname=shop'
    . ';charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

$pdo = new PDO($dsn, 'app_user', 'secret', $options);
Produktionsverbindung: Exceptions, assoziatives Fetch, native Vorbereitung.

Das Prepared Statement ist deine einzige strukturelle Verteidigung gegen SQL-Injection. Das Prinzip: Die Struktur der Abfrage mit ihren Platzhaltern :email oder ? wird zuerst an den Server geschickt; die Daten folgen getrennt und werden nie als SQL interpretiert. Eine Variable in eine Abfrage zu verketten, selbst eine "escapte", bleibt ein professioneller Fehler. Benannte Platzhalter machen den Code lesbar; positionsbezogene reichen für kurze Abfragen.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        email TEXT NOT NULL
    )'
);

$insert = $pdo->prepare(
    'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);

$query = $pdo->prepare(
    'SELECT id, email FROM users
     WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);

var_dump($query->fetch(PDO::FETCH_ASSOC));
Eigenständiges SQLite-Beispiel: Vorbereitung, Binding, Ausführung.

Eine Transaktion garantiert Atomarität: Entweder gelingen alle Schreibvorgänge, oder keiner davon. Das kanonische Muster: beginTransaction(), die Arbeit in einem try, commit() am Ende des Blocks, rollBack() im catch - und dann wirfst du die Exception erneut, denn den Fehlschlag zu verbergen wäre schlimmer als der Fehlschlag selbst. Halte Transaktionen kurz: Jede Sperre, die während eines Netzwerkaufrufs oder eines langen Vorgangs gehalten wird, verschlechtert die Nebenläufigkeit der gesamten Anwendung.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        balance INTEGER NOT NULL
    )'
);
$pdo->exec(
    'INSERT INTO accounts (balance)
     VALUES (100), (20)'
);

$pdo->beginTransaction();

try {
    $debit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance - :n
         WHERE id = :id'
    );
    $debit->execute(['n' => 50, 'id' => 1]);

    $credit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance + :n
         WHERE id = :id'
    );
    $credit->execute(['n' => 50, 'id' => 2]);

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

echo 'Transfer done';
Atomare Überweisung: Alles gelingt, oder alles wird zurückgerollt.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum blockiert ein Prepared Statement SQL-Injection?
    • Es escapt automatisch die Apostrophe in Zeichenketten
    • Die SQL-Struktur und die Daten reisen getrennt: Die Daten werden nie als SQL-Code interpretiert
    • Es verschlüsselt die Abfrage zwischen PHP und dem Server
  2. Was kannst du über einen Platzhalter :param in einem Prepared Statement übergeben?
    • Einen Wert: Zeichenkette, Zahl oder null
    • Einen Tabellen- oder Spaltennamen
    • Eine vollständige ORDER-BY-Klausel
  3. Was machst du im kanonischen Transaktionsmuster im catch-Block?
    • commit(), um zu sichern, was gelungen ist
    • rollBack(), dann die Exception erneut werfen
    • Nichts: Die Transaktion läuft von selbst ab