Menu

PHP PDO: mit MySQL verbinden und Prepared Statements

PDO ist die Datenbankschicht von PHP: Verbinde dich mit new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass) und führe dann jede Abfrage mit Werten über prepare() und execute([$value]) aus. Hier: Verbinden, Prepared Statements, Zeilen abrufen, Inserts, Transaktionen und WHERE IN.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

PDO (PHP Data Objects) ist der eingebaute Weg, mit einer Datenbank zu sprechen. Verbinde dich mit new PDO($dsn, $user, $password) und führe dann jede Abfrage, die einen Wert enthält, mit prepare() und execute() aus, damit der Wert getrennt vom SQL gesendet wird:

<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();      // an array, or false if no row matched

Die Beispiele auf dieser Seite brauchen einen Datenbankserver und stehen deshalb als einfacher Code da. Das Beispiel zur SQL-Injection weiter unten läuft.

Mit PDO zu MySQL verbinden

Das erste Argument ist ein DSN: der Treibername, dann die Verbindungsdetails. Leg die Verbindung an einer Stelle ab und binde sie dort ein, wo du sie brauchst.

<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // the default since PHP 8.0
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // rows as ['id' => 1, ...]
    PDO::ATTR_EMULATE_PREPARES   => false,                   // real prepared statements, real int types
];

try {
    $pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
    error_log($e->getMessage());
    http_response_code(500);
    exit('Database unavailable.');
}

return $pdo;
  • charset=utf8mb4 gehört in den DSN. Ohne kommen Umlaute, Akzente und Emoji als ? oder als verstümmelte Bytes an.
  • Gib $e->getMessage() nie an Besucher aus: Es kann Host und Benutzernamen enthalten. Protokolliere es.
  • Halte das Passwort aus dem Code heraus, in einer Umgebungsvariable oder einer Konfigurationsdatei außerhalb des Web-Roots.

Andere Datenbanken ändern nur den DSN: pgsql:host=localhost;dbname=shop für PostgreSQL, sqlite:/var/data/app.db für eine SQLite-Datei. Jede braucht ihren installierten PDO-Treiber (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m listet die auf, die du hast.

Prepared Statements mit ? und :name

Ein Platzhalter markiert, wo ein Wert hingehört. Verwende positionelle Platzhalter ? mit einer Liste oder benannte mit einem assoziativen Array:

<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);

// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);

// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();

Setze keine Anführungszeichen um einen Platzhalter: WHERE name = '?' vergleicht mit dem wörtlichen Text ?. Ein Prepared Statement lässt sich außerdem viele Male mit verschiedenen Werten ausführen, also bereitest du es einmal vor einer Schleife vor, nicht darin.

Warum String-Verkettung in SQL gefährlich ist

Das ist der eine Block auf der Seite, der läuft: Er baut die Abfrage für dieselbe Eingabe auf die falsche und auf die richtige Weise. Ändere $email und führe ihn erneut aus.

In der verketteten Abfrage schließt das Anführungszeichen in der Eingabe den String, und OR '1'='1' macht die Bedingung für jede Zeile wahr: Die Abfrage liefert alle Nutzer. Bei einem Login-Formular heißt das, sich anzumelden, ohne das Passwort zu kennen, und wenn der Treiber mehrere Anweisungen in einem Aufruf akzeptiert, führt eine Eingabe wie '; DROP TABLE users; -- eine zweite Anweisung nach Wahl des Angreifers aus. In der vorbereiteten Version bekommt die Datenbank SQL und Wert getrennt, also ist das Anführungszeichen nur ein Zeichen in einer E-Mail, die zu niemandem passt.

Escape-Funktionen wie addslashes() sind keine Lösung: Sie übersehen Fälle, die vom Zeichensatz der Verbindung abhängen. Verwende Platzhalter für jeden Wert, auch für Zahlen und für Werte, die „aus deinem eigenen Code kommen“.

Zeilen abrufen: fetch, fetchAll und fetchColumn

<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch();                 // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
    http_response_code(404);
}

// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();

// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
    echo $row['name'], "\n";
}

// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();

// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR);  // [42 => 'Ada', ...]

query() ist in Ordnung, wenn das SQL keine Werte von außen enthält. Sobald eine Variable beteiligt ist, wechselst du zu prepare().

Einfügen, aktualisieren und löschen

<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();

$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount();                 // rows changed

$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();

lastInsertId() liefert einen String, also castest du ihn. Bei MySQL zählt rowCount() nach einem UPDATE die Zeilen, die sich tatsächlich geändert haben, also ergibt das Setzen eines Namens auf den Wert, den er schon hat, 0. Speichere Passwörter mit password_hash, nie als Klartext.

Transaktionen

Eine Transaktion lässt mehrere Anweisungen gemeinsam gelingen oder scheitern. Wirft irgendetwas, rollst du zurück, und nichts wird gespeichert:

<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
    $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
    $pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);

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

Das setzt voraus, dass Fehler Exceptions werfen (seit PHP 8.0 der Standard), sodass eine fehlgeschlagene Anweisung ins catch springt. Bei MySQL müssen die Tabellen InnoDB verwenden; MyISAM ignoriert Transaktionen. Die Seite zu try/catch behandelt Throwable und erneutes Werfen.

WHERE IN mit einem Array

Ein einzelnes ? enthält einen einzelnen Wert, also funktioniert IN (?) mit einem Array nicht. Bau einen Platzhalter pro Wert:

<?php
$ids = [3, 8, 15];

if ($ids) {
    $placeholders = implode(',', array_fill(0, count($ids), '?'));   // "?,?,?"
    $stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
    $stmt->execute($ids);
    $products = $stmt->fetchAll();
} else {
    $products = [];                     // IN () would be a syntax error
}

Der String, der ins SQL eingesetzt wird, besteht nur aus Fragezeichen und Kommas, also bleibt die Abfrage sicher.

Typische Fehler mit Platzhaltern

  • Tabellen- und Spaltennamen. ORDER BY ? sortiert nach einem konstanten String, nicht nach der Spalte. Wähle Bezeichner aus einer Liste erlaubter Werte: $sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name'; und setze dann $sort ins SQL.
  • LIMIT ? mit emulierten Prepares. Mit dem standardmäßigen ATTR_EMULATE_PREPARES bei MySQL sendet execute(['10']) ein LIMIT '10', einen Syntaxfehler. Binde es mit PDO::PARAM_INT oder schalte die Emulation ab.
  • Derselbe benannte Platzhalter zweimal. WHERE first = :q OR last = :q funktioniert nur mit emulierten Prepares. Mit echten Prepares bekommst du SQLSTATE[HY093]: Invalid parameter number; verwende :q1 und :q2.
  • Vergessen, dass fetch() false liefert. $user['name'] auf false ist eine Warnung und null; prüfe zuerst das Ergebnis.

Häufig gestellte Fragen

Wie verbinde ich mich mit PDO zu MySQL?

Erzeuge ein PDO-Objekt mit DSN, Benutzer und Passwort: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Eine fehlgeschlagene Verbindung wirft eine PDOException; fang sie ab, protokolliere die Meldung und zeig dem Besucher einen allgemeinen Fehler.

Verhindern Prepared Statements SQL-Injection?

Ja, bei Werten: Mit prepare('... WHERE id = ?') und execute([$id]) wird der Wert als Daten gesendet und kann die Abfrage nie verändern. Platzhalter können nicht für Tabellennamen, Spaltennamen oder ASC/DESC stehen, also prüfst du diese gegen eine Liste erlaubter Namen, bevor du sie ins SQL setzt.

Was ist der Unterschied zwischen fetch und fetchAll in PDO?

fetch() liefert die nächste Zeile (oder false, wenn es keine mehr gibt), passt also für eine einzelne Zeile oder eine Schleife über ein großes Ergebnis. fetchAll() liefert alle Zeilen auf einmal in einem Array. fetchColumn() liefert einen Wert aus der nächsten Zeile, praktisch für SELECT COUNT(*).

Wie verwende ich WHERE IN mit einem Array in PDO?

Bau ein ? pro Wert und übergib das Array an execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Prüfe vorher, dass das Array nicht leer ist, denn IN () ist ein Syntaxfehler.

Sollte ich PDO oder mysqli verwenden?

Verwende PDO, außer du brauchst ein Feature, das es nur für MySQL gibt. Seine API ist für MySQL, PostgreSQL, SQLite und andere gleich, es unterstützt benannte Platzhalter und wirft seit PHP 8.0 standardmäßig Exceptions bei Fehlern. mysqli funktioniert nur mit MySQL und MariaDB.

Illustration der Programmiersprachen bei Coddy

Lerne mit Coddy zu programmieren

LOS GEHT'S