32 Datenbanken mit PDO

Die PHP Data Objects (PDO) Erweiterung ist eine konsistente Schnittstelle für den Datenbankzugriff in PHP. PDO bietet eine Abstraktionsschicht für den Datenbankzugriff, die unabhängig von der verwendeten Datenbank ist. Dadurch wird der Wechsel zwischen verschiedenen Datenbanksystemen erheblich erleichtert.

32.1 Vorteile von PDO

PDO bietet gegenüber älteren Datenbankschnittstellen wie mysql_* oder mysqli_* folgende Vorteile:

32.2 Verbindung zur Datenbank herstellen

Um eine Verbindung zu einer Datenbank herzustellen, verwenden wir den PDO-Konstruktor:

<?php
try {
    // MySQL-Verbindung
    $pdo = new PDO(
        'mysql:host=localhost;dbname=meine_datenbank;charset=utf8mb4',
        'benutzername',
        'passwort',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
    
    echo "Verbindung erfolgreich hergestellt!";
} catch (PDOException $e) {
    echo "Verbindungsfehler: " . $e->getMessage();
    exit;
}

Die Optionen in diesem Beispiel sind besonders wichtig:

32.3 DSN für verschiedene Datenbanksysteme

Der Data Source Name (DSN) unterscheidet sich je nach Datenbanksystem:

<?php
// MySQL/MariaDB
$dsn = 'mysql:host=localhost;dbname=meine_datenbank;charset=utf8mb4';

// PostgreSQL
$dsn = 'pgsql:host=localhost;dbname=meine_datenbank';

// SQLite
$dsn = 'sqlite:/pfad/zur/datenbank.sqlite';

// MS SQL Server
$dsn = 'sqlsrv:Server=localhost;Database=meine_datenbank';

// Oracle
$dsn = 'oci:dbname=//localhost:1521/XE';

32.4 Einfache Abfragen ausführen

Mit der query()-Methode können einfache SQL-Abfragen ohne Parameter ausgeführt werden:

<?php
try {
    // Alle Benutzer abrufen
    $stmt = $pdo->query('SELECT id, name, email FROM benutzer');
    
    // Ergebnisse durchlaufen
    while ($row = $stmt->fetch()) {
        echo "ID: {$row['id']}, Name: {$row['name']}, E-Mail: {$row['email']}<br>";
    }
} catch (PDOException $e) {
    echo "Fehler: " . $e->getMessage();
}

32.5 Prepared Statements

Prepared Statements sind der sicherste Weg, um Benutzerangaben in SQL-Abfragen zu verwenden. Sie schützen vor SQL-Injection-Angriffen:

<?php
try {
    // Vorbereiten des Statements
    $stmt = $pdo->prepare('SELECT id, name, email FROM benutzer WHERE id = :id');
    
    // Parameter binden
    $stmt->bindParam(':id', $id, PDO::PARAM_INT);
    $id = 5;
    
    // Ausführen
    $stmt->execute();
    
    // Ergebnis abrufen
    $benutzer = $stmt->fetch();
    
    if ($benutzer) {
        echo "Gefundener Benutzer: {$benutzer['name']}";
    } else {
        echo "Kein Benutzer mit dieser ID gefunden.";
    }
} catch (PDOException $e) {
    echo "Fehler: " . $e->getMessage();
}

Alternativ können Parameter auch direkt bei der Ausführung übergeben werden:

<?php
$stmt = $pdo->prepare('SELECT * FROM benutzer WHERE email = :email AND status = :status');
$stmt->execute(['email' => 'max@example.com', 'status' => 'aktiv']);
$benutzer = $stmt->fetch();

32.6 Daten einfügen

Zum Einfügen von Daten wird ebenfalls prepare() und execute() verwendet:

<?php
try {
    $stmt = $pdo->prepare('
        INSERT INTO benutzer (name, email, passwort, erstellt_am) 
        VALUES (:name, :email, :passwort, :erstellt_am)
    ');
    
    $stmt->execute([
        'name' => 'Max Mustermann',
        'email' => 'max@example.com',
        'passwort' => password_hash('sicheres_passwort', PASSWORD_DEFAULT),
        'erstellt_am' => date('Y-m-d H:i:s')
    ]);
    
    $neueId = $pdo->lastInsertId();
    echo "Neuer Benutzer mit ID $neueId erstellt!";
} catch (PDOException $e) {
    echo "Fehler beim Einfügen: " . $e->getMessage();
}

32.7 Daten aktualisieren

Das Aktualisieren von Daten folgt dem gleichen Muster:

<?php
try {
    $stmt = $pdo->prepare('
        UPDATE benutzer 
        SET name = :name, email = :email 
        WHERE id = :id
    ');
    
    $erfolg = $stmt->execute([
        'name' => 'Maximilian Mustermann',
        'email' => 'maximilian@example.com',
        'id' => 5
    ]);
    
    echo "Betroffene Zeilen: " . $stmt->rowCount();
} catch (PDOException $e) {
    echo "Fehler beim Aktualisieren: " . $e->getMessage();
}

32.8 Daten löschen

Auch das Löschen von Daten erfolgt mit Prepared Statements:

<?php
try {
    $stmt = $pdo->prepare('DELETE FROM benutzer WHERE id = :id');
    $stmt->execute(['id' => 5]);
    
    if ($stmt->rowCount() > 0) {
        echo "Benutzer erfolgreich gelöscht!";
    } else {
        echo "Kein Benutzer mit dieser ID gefunden.";
    }
} catch (PDOException $e) {
    echo "Fehler beim Löschen: " . $e->getMessage();
}

32.9 Verschiedene Methoden zum Abrufen von Ergebnissen

PDO bietet verschiedene Methoden, um Ergebnisse abzurufen:

<?php
$stmt = $pdo->query('SELECT id, name, email FROM benutzer LIMIT 10');

// Einzelne Zeile
$zeile = $stmt->fetch(); // Assoziatives Array (entsprechend der Konfiguration)

// Alle Ergebnisse auf einmal als Array
$alle = $stmt->fetchAll();

// Einzelne Spalte der nächsten Zeile
$name = $stmt->fetchColumn(1); // Zweite Spalte (index 1)

// Ergebnis als Objekt mit benannten Eigenschaften
$stmt->setFetchMode(PDO::FETCH_OBJ);
$benutzer = $stmt->fetch();
echo $benutzer->name; // Zugriff als Objekt

// Ergebnis direkt in eine Klasse
class Benutzer {
    public $id;
    public $name;
    public $email;
    
    public function vollständigerName() {
        return "Benutzer: " . $this->name;
    }
}

$stmt->setFetchMode(PDO::FETCH_CLASS, 'Benutzer');
$benutzer = $stmt->fetch();
echo $benutzer->vollständigerName();

32.10 Transaktionen

Transaktionen ermöglichen es, mehrere Datenbankoperationen als atomare Einheit auszuführen:

<?php
try {
    // Transaktion starten
    $pdo->beginTransaction();
    
    // Erste Operation
    $stmt1 = $pdo->prepare('UPDATE konten SET guthaben = guthaben - :betrag WHERE id = :von_id');
    $stmt1->execute(['betrag' => 100.00, 'von_id' => 1]);
    
    // Zweite Operation
    $stmt2 = $pdo->prepare('UPDATE konten SET guthaben = guthaben + :betrag WHERE id = :zu_id');
    $stmt2->execute(['betrag' => 100.00, 'zu_id' => 2]);
    
    // Überprüfen, ob Konto nicht überzogen wird
    $stmt3 = $pdo->prepare('SELECT guthaben FROM konten WHERE id = :id');
    $stmt3->execute(['id' => 1]);
    $guthaben = $stmt3->fetchColumn();
    
    if ($guthaben < 0) {
        // Abbrechen, wenn Konto überzogen würde
        throw new Exception('Nicht genügend Guthaben vorhanden!');
    }
    
    // Wenn alles erfolgreich, Transaktion bestätigen
    $pdo->commit();
    echo "Überweisung erfolgreich durchgeführt!";
} catch (Exception $e) {
    // Bei Fehler Transaktion zurückrollen
    $pdo->rollBack();
    echo "Fehler bei der Überweisung: " . $e->getMessage();
}

32.11 Mit großen Datenmengen umgehen

Bei der Verarbeitung großer Datenmengen sollten die Ergebnisse schrittweise abgerufen werden, um den Speicherverbrauch zu minimieren:

<?php
// Viele Datensätze verarbeiten
$stmt = $pdo->prepare('SELECT id, name, email FROM benutzer');
$stmt->execute();

// Statt fetchAll() zu verwenden, was alle Ergebnisse auf einmal lädt
// Ergebnisse einzeln durchlaufen
while ($row = $stmt->fetch()) {
    // Verarbeite jede Zeile einzeln
    processUser($row);
}

32.12 SQL-Injection vermeiden

PDO hilft bei der Vermeidung von SQL-Injection, aber es ist wichtig, die Funktionen richtig zu verwenden:

<?php
// UNSICHER - NIE SO MACHEN!
$id = $_GET['id'];
$query = "SELECT * FROM benutzer WHERE id = $id"; // SQL-Injection möglich!
$stmt = $pdo->query($query);

// SICHER - Immer Prepared Statements verwenden
$stmt = $pdo->prepare('SELECT * FROM benutzer WHERE id = :id');
$stmt->execute(['id' => $_GET['id']]);

// SICHER - Auch für dynamische Tabellenoperationen
$sortierFeld = in_array($_GET['sort'], ['name', 'email', 'erstellt_am']) 
    ? $_GET['sort'] 
    : 'name';
$stmt = $pdo->prepare("SELECT * FROM benutzer ORDER BY $sortierFeld");
$stmt->execute();

Für dynamische Tabellen- oder Spaltennamen, die nicht als Parameter übergeben werden können, muss eine Whitelist verwendet werden:

<?php
// Whitelist für erlaubte Tabellennamen
$erlaubteTabellenNamen = ['benutzer', 'artikel', 'kategorien'];
$tabelle = $_GET['tabelle'];

if (in_array($tabelle, $erlaubteTabellenNamen)) {
    $stmt = $pdo->prepare("SELECT * FROM $tabelle");
    $stmt->execute();
} else {
    echo "Unerlaubter Tabellenname!";
}

32.13 Ein einfaches Datenbank-Wrapper-Beispiel

In realen Anwendungen wird oft ein Wrapper um PDO erstellt, um häufige Operationen zu vereinfachen:

<?php
class DB {
    private $pdo;
    
    public function __construct($dsn, $username, $password, $options = []) {
        $defaultOptions = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ];
        
        $this->pdo = new PDO(
            $dsn, 
            $username, 
            $password, 
            array_merge($defaultOptions, $options)
        );
    }
    
    public function run($sql, $params = []) {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }
    
    public function findAll($table, $where = [], $orderBy = null, $limit = null) {
        $sql = "SELECT * FROM $table";
        
        if (!empty($where)) {
            $whereClauses = [];
            foreach (array_keys($where) as $key) {
                $whereClauses[] = "$key = :$key";
            }
            $sql .= " WHERE " . implode(' AND ', $whereClauses);
        }
        
        if ($orderBy) {
            $sql .= " ORDER BY $orderBy";
        }
        
        if ($limit) {
            $sql .= " LIMIT $limit";
        }
        
        $stmt = $this->run($sql, $where);
        return $stmt->fetchAll();
    }
    
    public function find($table, $where = []) {
        $result = $this->findAll($table, $where, null, 1);
        return $result ? $result[0] : null;
    }
    
    public function insert($table, $data) {
        $keys = array_keys($data);
        $fields = implode(', ', $keys);
        $placeholders = ':' . implode(', :', $keys);
        
        $sql = "INSERT INTO $table ($fields) VALUES ($placeholders)";
        $this->run($sql, $data);
        
        return $this->pdo->lastInsertId();
    }
    
    public function update($table, $data, $where) {
        $setClauses = [];
        foreach (array_keys($data) as $key) {
            $setClauses[] = "$key = :set_$key";
        }
        $sql = "UPDATE $table SET " . implode(', ', $setClauses);
        
        $whereClauses = [];
        foreach (array_keys($where) as $key) {
            $whereClauses[] = "$key = :where_$key";
        }
        $sql .= " WHERE " . implode(' AND ', $whereClauses);
        
        $params = [];
        foreach ($data as $key => $value) {
            $params["set_$key"] = $value;
        }
        foreach ($where as $key => $value) {
            $params["where_$key"] = $value;
        }
        
        $stmt = $this->run($sql, $params);
        return $stmt->rowCount();
    }
    
    public function delete($table, $where) {
        $whereClauses = [];
        foreach (array_keys($where) as $key) {
            $whereClauses[] = "$key = :$key";
        }
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', $whereClauses);
        
        $stmt = $this->run($sql, $where);
        return $stmt->rowCount();
    }
    
    public function beginTransaction() {
        return $this->pdo->beginTransaction();
    }
    
    public function commit() {
        return $this->pdo->commit();
    }
    
    public function rollBack() {
        return $this->pdo->rollBack();
    }
}

Verwendung des Wrappers:

<?php
$db = new DB(
    'mysql:host=localhost;dbname=meine_datenbank;charset=utf8mb4',
    'benutzername',
    'passwort'
);

// Alle aktiven Benutzer finden
$aktiveBenutzer = $db->findAll('benutzer', ['status' => 'aktiv'], 'name ASC');

// Einen Benutzer finden
$benutzer = $db->find('benutzer', ['email' => 'max@example.com']);

// Neuen Benutzer einfügen
$neueId = $db->insert('benutzer', [
    'name' => 'Anna Müller',
    'email' => 'anna@example.com',
    'status' => 'aktiv'
]);

// Benutzer aktualisieren
$db->update(
    'benutzer',
    ['name' => 'Anna Schmidt', 'status' => 'inaktiv'],
    ['id' => 5]
);

// Benutzer löschen
$db->delete('benutzer', ['id' => 5]);