Werken met databases in PHP met PDO: complete gids

Leer hoe je met PDO veilig databases benadert in PHP. Praktische uitleg over prepared statements, transacties en foutafhandeling voor developers.

8 juni 20266 min leestijdDoor We Develop Communication

Wanneer je een webapplicatie bouwt die gebruikersgegevens, producten of berichten moet opslaan, heb je vroeg of laat een database nodig. In PHP is PDO (PHP Data Objects) de moderne manier om met databases te werken. Het biedt een veilige, flexibele en database-onafhankelijke manier om queries uit te voeren.

In deze gids leer je hoe je met PDO een verbinding opzet, queries uitvoert, prepared statements gebruikt en transacties beheert. We bouwen voort op concepten die je eerder hebt geleerd in onze serie over object-oriented PHP en functies en scope.

Wat is PDO precies?

PDO staat voor PHP Data Objects en is een database-abstractielaag die sinds PHP 5.1 onderdeel is van de core. In plaats van databasespecifieke functies zoals mysql_query() of mysqli_query() te gebruiken, werk je met één consistente objectgeoriënteerde API.

Het grote voordeel: je kunt dezelfde code gebruiken voor MySQL, PostgreSQL, SQLite, Oracle en meer. Alleen de connectiestring verandert.

Waarom kiezen voor PDO?

  • Database-onafhankelijk: wissel eenvoudig tussen databases zonder je code volledig te herschrijven
  • Prepared statements: ingebouwde bescherming tegen SQL-injectie
  • Objectgeoriënteerd: past natuurlijk in moderne PHP-code
  • Uitgebreide foutafhandeling: via exceptions in plaats van foutcodes
  • Genamed parameters: leesbaardere queries met :name in plaats van ?

De oude mysql_*-functies zijn sinds PHP 7 volledig verwijderd. MySQLi bestaat nog wel, maar werkt alleen met MySQL. Voor nieuwe projecten is PDO bijna altijd de betere keuze.

Een verbinding opzetten met PDO

Om met een database te werken, maak je eerst een PDO-object aan. Dit object vertegenwoordigt de verbinding.

<?php
$dsn = 'mysql:host=localhost;dbname=mijn_database;charset=utf8mb4';
$username = 'db_user';
$password = 'geheim_wachtwoord';

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

try {
    $pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
    throw new RuntimeException('Database-verbinding mislukt: ' . $e->getMessage());
}

De belangrijkste opties uitgelegd

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION Zorgt ervoor dat PDO bij fouten een PDOException gooit. Dit is cruciaal voor moderne foutafhandeling.

PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC Zorgt dat resultaten standaard als associatieve arrays worden teruggegeven in plaats van zowel numeriek als associatief (wat geheugen verspilt).

PDO::ATTR_EMULATE_PREPARES => false Dwingt echte prepared statements af, in plaats van client-side emulatie. Dit is veiliger en accurater qua datatypes.

Voor meer achtergrond over de verschillen tussen deze modi raden we de officiële PHP-documentatie over PDO aan.

Queries uitvoeren zonder gebruikersinvoer

Voor queries zonder dynamische waarden kun je query() gebruiken. Denk aan het ophalen van alle records of statische lijsten.

<?php
$stmt = $pdo->query('SELECT id, naam, email FROM gebruikers ORDER BY naam');

foreach ($stmt as $row) {
    echo $row['naam'] . ', ' . $row['email'] . PHP_EOL;
}

Omdat we PDO::FETCH_ASSOC als standaard hebben ingesteld, krijg je keurige associatieve arrays terug. Als je al bekend bent met arrays en loops in PHP, voelt dit direct vertrouwd aan.

Alle rijen in één keer ophalen

Soms wil je het complete resultaat als array:

<?php
$gebruikers = $pdo->query('SELECT * FROM gebruikers')->fetchAll();

Let op: fetchAll() laadt alles in het geheugen. Bij grote datasets gebruik je beter een loop met fetch().

Prepared statements: je eerste verdediging tegen SQL-injectie

Zodra er gebruikersinvoer in het spel komt, is query() niet meer veilig. Hier komen prepared statements in beeld.

Het probleem: SQL-injectie

Dit is wat je nooit mag doen:

<?php
// GEVAARLIJK, doe dit NOOIT
$email = $_POST['email'];
$stmt = $pdo->query("SELECT * FROM gebruikers WHERE email = '$email'");

Als een kwaadwillende ' OR '1'='1 invult, krijgt deze toegang tot alle gebruikers.

De oplossing: named parameters

<?php
$email = $_POST['email'];

$stmt = $pdo->prepare('SELECT * FROM gebruikers WHERE email = :email');
$stmt->execute([':email' => $email]);

$gebruiker = $stmt->fetch();

if ($gebruiker) {
    echo 'Welkom terug, ' . $gebruiker['naam'];
} else {
    echo 'Geen gebruiker gevonden.';
}

PDO houdt de query en de waarden strikt gescheiden. De database behandelt $email altijd als data, nooit als SQL-code.

Positionele parameters als alternatief

Je kunt ook vraagtekens gebruiken:

<?php
$stmt = $pdo->prepare('SELECT * FROM gebruikers WHERE email = ? AND actief = ?');
$stmt->execute([$email, 1]);
$gebruiker = $stmt->fetch();

Named parameters zijn meestal leesbaarder, vooral bij queries met veel variabelen.

Data invoegen, updaten en verwijderen

De principes blijven hetzelfde voor INSERT, UPDATE en DELETE. Je gebruikt altijd prepared statements.

Een nieuwe gebruiker toevoegen

<?php
$stmt = $pdo->prepare(
    'INSERT INTO gebruikers (naam, email, aangemaakt_op)
     VALUES (:naam, :email, NOW())'
);

$stmt->execute([
    ':naam' => 'Eva Jansen',
    ':email' => '[email protected]',
]);

$nieuweId = (int) $pdo->lastInsertId();
echo "Gebruiker aangemaakt met ID: $nieuweId";

De methode lastInsertId() geeft het auto-increment ID terug van de zojuist ingevoegde rij.

Een record bijwerken

<?php
$stmt = $pdo->prepare(
    'UPDATE gebruikers SET email = :email WHERE id = :id'
);
$stmt->execute([
    ':email' => '[email protected]',
    ':id' => 42,
]);

echo $stmt->rowCount() . ' rij(en) bijgewerkt.';

rowCount() geeft het aantal beïnvloede rijen terug, handig om te bevestigen dat de update echt iets heeft gedaan.

Records verwijderen

<?php
$stmt = $pdo->prepare('DELETE FROM gebruikers WHERE id = :id');
$stmt->execute([':id' => 42]);

Transacties: alles of niets

Soms moet je meerdere queries uitvoeren die logisch bij elkaar horen. Denk aan een bestelling: je wilt de bestelling opslaan én de voorraad afboeken. Als één van beide faalt, moeten ze allebei teruggedraaid worden.

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

    $stmt = $pdo->prepare(
        'INSERT INTO bestellingen (gebruiker_id, totaal) VALUES (:id, :totaal)'
    );
    $stmt->execute([':id' => 1, ':totaal' => 49.95]);
    $bestellingId = $pdo->lastInsertId();

    $stmt = $pdo->prepare(
        'UPDATE producten SET voorraad = voorraad - :aantal WHERE id = :id'
    );
    $stmt->execute([':aantal' => 2, ':id' => 15]);

    $pdo->commit();
    echo "Bestelling $bestellingId opgeslagen.";
} catch (PDOException $e) {
    $pdo->rollBack();
    throw $e;
}

Zonder transactie loop je het risico dat een bestelling bestaat zonder dat de voorraad is bijgewerkt, of andersom. Dat leidt tot inconsistente data.

Resultaten verwerken: fetch-modi

PDO biedt meerdere manieren om rijen op te halen.

FETCH_ASSOC (standaard in dit voorbeeld)

<?php
$row = $stmt->fetch(); // ['id' => 1, 'naam' => 'Eva']

FETCH_OBJ, als object

<?php
$row = $stmt->fetch(PDO::FETCH_OBJ);
echo $row->naam;

FETCH_CLASS, direct naar een eigen class

<?php
class Gebruiker {
    public int $id;
    public string $naam;
    public string $email;
}

$stmt = $pdo->query('SELECT id, naam, email FROM gebruikers');
$gebruikers = $stmt->fetchAll(PDO::FETCH_CLASS, Gebruiker::class);

foreach ($gebruikers as $gebruiker) {
    echo $gebruiker->naam;
}

Deze laatste techniek werkt mooi samen met de objectgeoriënteerde aanpak die we behandelden in object-oriented PHP basics.

Foutafhandeling in de praktijk

Met PDO::ERRMODE_EXCEPTION gooit elke fout een PDOException. Je kunt deze centraal afhandelen.

<?php
try {
    $stmt = $pdo->prepare('SELECT * FROM gebruikers WHERE id = :id');
    $stmt->execute([':id' => $id]);
    $gebruiker = $stmt->fetch();
} catch (PDOException $e) {
    error_log('Database-fout: ' . $e->getMessage());
    http_response_code(500);
    echo 'Er ging iets mis. Probeer het later opnieuw.';
    exit;
}

Belangrijk: toon nooit $e->getMessage() rechtstreeks aan eindgebruikers. Deze berichten bevatten vaak tabelnamen, kolommen of zelfs query-fragmenten die gevoelige informatie kunnen lekken.

PDO in een herbruikbare class verpakken

In plaats van overal een nieuw PDO-object te maken, wil je meestal een centrale database-class. Een simpele singleton-achtige opzet:

<?php
class Database {
    private static ?PDO $instance = null;

    public static function connect(): PDO {
        if (self::$instance === null) {
            $dsn = 'mysql:host=' . getenv('DB_HOST')
                . ';dbname=' . getenv('DB_NAME')
                . ';charset=utf8mb4';

            self::$instance = new PDO(
                $dsn,
                getenv('DB_USER'),
                getenv('DB_PASS'),
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
        }

        return self::$instance;
    }
}

// Gebruik
$pdo = Database::connect();

In grotere projecten gebruik je liever dependency injection via een container, bijvoorbeeld geleverd door een framework of pakket dat je installeert met Composer.

Veelgemaakte fouten om te vermijden

  • String-concatenatie in queries, gebruik altijd prepared statements, zelfs voor "ongevaarlijke" waarden
  • Geen error mode instellen, zonder ERRMODE_EXCEPTION verdwijnen fouten stilletjes
  • Wachtwoorden hardcoden, gebruik environment variables (bijvoorbeeld via phpdotenv)
  • Te veel data in één keer ophalen, gebruik fetch() in een loop voor grote resultaten
  • Vergeten om transacties te gebruiken bij meerdere samenhangende queries

Conclusie

PDO is de standaard voor database-werk in moderne PHP. Het geeft je een veilige, consistente en flexibele API die met vrijwel elke database samenwerkt. Met prepared statements bescherm je je applicatie tegen SQL-injectie, en met transacties houd je je data consistent.

Begin klein: zet een connectie op, oefen met prepared statements en bouw van daaruit verder. In de volgende artikelen van deze serie kijken we naar hoe je database-queries kunt abstraheren met repository-patterns en hoe je migraties beheert.

Veelgestelde vragen

Wat is PDO in PHP?

PDO (PHP Data Objects) is een database-abstractielaag in PHP waarmee je met één uniforme API verschillende database-systemen kunt benaderen, zoals MySQL, PostgreSQL en SQLite. Het biedt ingebouwde ondersteuning voor prepared statements en veilige foutafhandeling.

Wat is het verschil tussen PDO en MySQLi?

PDO werkt met meerdere database-systemen (MySQL, PostgreSQL, SQLite, etc.), terwijl MySQLi alleen MySQL ondersteunt. PDO biedt een consistentere API en is flexibeler, maar MySQLi heeft enkele MySQL-specifieke features zoals async queries.

Waarom zou ik prepared statements gebruiken?

Prepared statements beschermen je applicatie tegen SQL-injectie door gebruikersinvoer strikt gescheiden te houden van de SQL-query. Daarnaast kunnen ze prestatie-voordelen opleveren als je dezelfde query meerdere keren uitvoert met verschillende parameters.

Hoe vang ik PDO-fouten op?

Zet de error mode op PDO::ERRMODE_EXCEPTION bij het openen van de connectie. Vervolgens kun je database-fouten opvangen met try/catch-blokken die een PDOException afhandelen. Dit voorkomt dat gevoelige foutinformatie wordt gelekt.

Is PDO langzamer dan directe MySQL-functies?

In de praktijk is het prestatieverschil verwaarloosbaar. De voordelen van veiligheid, leesbaarheid en portabiliteit wegen zwaarder dan een minimale overhead. Voor zwaar belaste applicaties maakt correcte indexering en query-optimalisatie veel meer verschil.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.