Databaseverbinding in PHP: PDO, prepared statements en mysqli

Verbinding maken met MySQL of MariaDB in PHP 8.5: de DSN van PDO, de charset, prepared statements, het lekken van foutmeldingen voorkomen en wat er sinds PHP 8.1 met mysqli is veranderd.

Databaseverbinding in PHP: PDO, prepared statements en mysqli
Kort antwoord

Gebruik PDO met charset=utf8mb4 in de DSN, ATTR_ERRMODE op exception en ATTR_EMULATE_PREPARES op false. Sinds PHP 8.0 gooien PDO-fouten al standaard een exception, en sinds PHP 8.1 doet mysqli dat ook: het patroon if (!$conn) die(...) slaat nooit meer aan. Elke waarde die van de gebruiker komt, gaat door een prepared statement.

Verbinding maken met een database is het eerste wat de meeste PHP-applicaties doen. De keuze van de extensie en de manier waarop je die verbinding schrijft bepalen twee dingen: hoe leesbaar de fouten zijn op de dag dat het misgaat, en hoe goed de code SQL-injecties weerstaat. Dit artikel laat de verbinding zien zoals je hem in 2026 schrijft, op PHP 8.5, met de valkuilen die sinds PHP 8.0 zijn veranderd.

PDO, de verbinding om te onthouden

PDO is de generieke database-interface van PHP. Hij praat met MySQL en MariaDB, maar net zo goed met PostgreSQL, SQLite of SQL Server, met dezelfde applicatiecode. Dat is de standaardkeuze, tenzij je een concrete reden hebt om het anders te doen.

src/Base.php
<?php

declare(strict_types=1);

$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=ma_base;charset=utf8mb4';

$pdo = new PDO($dsn, 'utilisateur', 'mot_de_passe', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
]);

Drie details tellen in dit blok, en het zijn precies de details die je vergeet.

  • charset=utf8mb4 in de DSN. Zonder die parameter gebruikt de verbinding de standaardtekenset van de server. Emoji en een deel van de Aziatische tekens worden vraagtekens, en bij sommige tekensets wordt de encoding zelf een injectievector. De charset hoort in de DSN, niet in een query SET NAMES.
  • ATTR_EMULATE_PREPARES op false. Standaard simuleert de MySQL-driver de prepared statements aan de PHP-kant in plaats van ze naar de server te sturen. Zet je die emulatie uit, dan scheidt de server zelf de query van de data, en blijven integers ook bij het teruglezen integers.
  • Een expliciete ATTR_ERRMODE. Sinds PHP 8.0 is ERRMODE_EXCEPTION al de standaardwaarde. Hem laten staan verandert niets aan het gedrag, maar vertelt de lezer van de code dat fouten als exceptions naar boven komen.

Controle op de testmachine:

php
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // geen opties
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) op PHP 8.5.10

Het wachtwoord hoort niet in het bestand

Inloggegevens hebben niets te zoeken in een bestand dat onder versiebeheer staat. De eenvoudigste vorm leest ze uit de omgeving, met een duidelijke melding als de variabele ontbreekt.

src/connexion.php
<?php

declare(strict_types=1);

function connexion(): PDO
{
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }

    $hote = getenv('DB_HOST') ?: '127.0.0.1';
    $base = getenv('DB_NAME') ?: throw new RuntimeException('DB_NAME manquant');
    $user = getenv('DB_USER') ?: throw new RuntimeException('DB_USER manquant');
    $pass = getenv('DB_PASS') ?: '';

    return $pdo = new PDO(
        "mysql:host={$hote};dbname={$base};charset=utf8mb4",
        $user,
        $pass,
        [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ],
    );
}

De operator ?: gevolgd door throw werkt sinds PHP 8.0, waar throw een expressie is geworden. De verbinding wordt in een statische variabele bewaard: één verbinding per request is ruimschoots genoeg.

Laat de foutmelding nooit lekken

Een verbindingsexceptie bevat de host, de gebruiker en soms het wachtwoord in de stacktrace. Zo op een publieke pagina getoond geef je een aanvaller het halve werk cadeau.

php
try {
    $pdo = connexion();
} catch (PDOException $e) {
    // De details gaan naar het logbestand, nooit naar de HTTP-respons
    error_log('Connexion base impossible : ' . $e->getMessage());
    http_response_code(503);
    exit('Service momentanément indisponible.');
}

Sinds PHP 8.2 verschijnen parameters die met #[SensitiveParameter] zijn gemarkeerd als Object(SensitiveParameterValue) in de traces. Dat is een nuttige bescherming, maar ze dekt alleen de trace: de melding zelf blijft spraakzaam.

Verbinding maken is maar het halve werk

Een verbinding zonder prepared statement beschermt nergens tegen. De regel past in één zin: data wordt nooit in de query geplakt.

php
// Zo moet het niet: de waarde gaat rechtstreeks de query in
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";

// Prepared statement: de waarde reist apart
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();

Benoemde placeholders lezen prettiger zodra er meer dan twee waarden in het spel zijn:

php
$requete = $pdo->prepare(
    'SELECT id, titre FROM articles
     WHERE statut = :statut AND publie_le >= :depuis
     ORDER BY publie_le DESC LIMIT :limite'
);
$requete->bindValue(':statut', 'publie');
$requete->bindValue(':depuis', '2026-01-01');
$requete->bindValue(':limite', 10, PDO::PARAM_INT); // typeer de LIMIT altijd
$requete->execute();

foreach ($requete as $ligne) {
    echo $ligne['titre'], "\n";
}

De PDO::PARAM_INT op de LIMIT is geen versiering, en het effect ervan hangt af van een instelling die vaak wordt vergeten. Dit is wat bindValue(':limite', 3) zonder typering oplevert, afhankelijk van of de emulatie van prepared statements aanstaat:

Server EMULATE_PREPARES Zonder PARAM_INT Met PARAM_INT
MariaDB 11.8.9 false geaccepteerd geaccepteerd
MariaDB 11.8.9 true (standaard) SQL-fout 1064 geaccepteerd
MySQL 8.4.11 false geaccepteerd geaccepteerd
MySQL 8.4.11 true (standaard) SQL-fout 1064 geaccepteerd

Met emulatie bouwt PDO de query zelf en zet hij aanhalingstekens om de waarde: LIMIT '3', wat de server weigert. Dat is de standaardconfiguratie, dus het geval dat de meeste mensen tegenkomen. De waarde typeren werkt in alle vier de situaties, dat is de enige vorm die je hoeft te onthouden.

Een placeholder vervangt nooit een tabel- of kolomnaam. Komt de kolomnaam van de gebruiker, dan valideer je hem tegen een gesloten lijst:

php
$colonnesTriables = ['titre', 'publie_le', 'vues'];
$tri = in_array($_GET['tri'] ?? '', $colonnesTriables, true) ? $_GET['tri'] : 'publie_le';
$sql = "SELECT id, titre FROM articles ORDER BY {$tri} DESC";

En mysqli dan?

mysqli blijft bruikbaar als het project nooit met iets anders dan MySQL of MariaDB gaat praten. Het gedrag is op één belangrijk punt veranderd: sinds PHP 8.1 staat mysqli_report() op MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, en gooien fouten een exception in plaats van false terug te geven.

php
<?php

declare(strict_types=1);

try {
    $mysqli = new mysqli('127.0.0.1', $utilisateur, $motDePasse, 'ma_base');
    $mysqli->set_charset('utf8mb4');
} catch (mysqli_sql_exception $e) {
    error_log('Connexion mysqli impossible : ' . $e->getMessage());
    http_response_code(503);
    exit('Service momentanément indisponible.');
}

$requete = $mysqli->prepare('SELECT id, email FROM membres WHERE email = ?');
$requete->bind_param('s', $email);
$requete->execute();
$membre = $requete->get_result()->fetch_assoc();

De verbindingstest die niets meer test

Het volgende patroon zwerft door talloze tutorials, ook door de eerdere versies van deze. Het werkt niet meer sinds PHP 8.1.

php
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);

if (!$conn) {
    die('La connexion a échoué : ' . mysqli_connect_error());
}

Uitgevoerd op PHP 8.5.10 met een verkeerd wachtwoord levert deze code niet de bedoelde melding op. Hij levert dit op:

code
PHP Fatal error:  Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8

De regel if (!$conn) wordt nooit bereikt: mysqli_connect() heeft de exception al gegooid. De die() is dode code, en de ruwe fout vertrekt naar de browser zodra het tonen van fouten aanstaat. De herschrijving met try / catch hierboven is vandaag de enige juiste vorm.

De mysql_*-functies bestaan niet meer

De mysql_connect(), mysql_query() en mysql_real_escape_string() die je nog in oude scripts tegenkomt, zijn verwijderd uit PHP 7.0, uitgebracht in 2015. Op PHP 8.5:

php
var_dump(function_exists('mysql_connect'));           // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)

Er valt geen compatibiliteit te bewaren, alleen een migratie uit te voeren, bij voorkeur naar PDO.

Verder lezen

Voor het invoegen van een groot volume rijen is de verbinding maar de helft van het onderwerp: zie grote bestanden lezen in PHP, dat het invoegen per batch binnen een transactie laat zien. Om na een verwerking door te sturen, zie een redirect maken in PHP. Zodra de verbinding staat, bestaat het vervolg eruit hem niet in elk bestand opnieuw op te bouwen. De andere PHP-tutorials op de hub Webdevelopment behandelen de aangrenzende bouwstenen: Composer installeren om het project te ordenen, en de basis van OOP in PHP om deze verbinding in een klasse onder te brengen in plaats van in een globale functie.

Veelgemaakte fouten

De die() die nooit wordt uitgevoerd Sinds PHP 8.1 gooit mysqli_connect() een mysqli_sql_exception voordat if (!$conn) wordt bereikt. De ruwe foutmelding vertrekt naar de browser in plaats van de bedoelde melding.
Charset ontbreekt in de DSN Zonder charset=utf8mb4 worden emoji en sommige tekens vraagtekens, en kan de encoding als injectievector dienen.
LIMIT zonder PDO::PARAM_INT Met de emulatie van prepared statements, die standaard aanstaat, schrijft PDO LIMIT '3' en geeft de server SQL-fout 1064. Gecontroleerd op MariaDB 11.8.9 en MySQL 8.4.11.
Exceptiemelding getoond De trace van een PDOException bevat de host en de gebruiker. Die gaat naar het logbestand, nooit naar de HTTP-respons.
Placeholder voor een kolomnaam Een placeholder vervangt alleen een waarde. Een tabel- of kolomnaam valideer je tegen een gesloten lijst.

MySQLPDOPHPSécurité

Damien Flandrin Webdeveloper sinds 2010, maker van Gekkode en Email Impact. Elk artikel wordt vóór publicatie getest op een echt project. Contact
Nieuwsbrief

Nieuwe tests, tutorials en projecten, per e-mail.

Reproduceerbare tests, geversioneerde code, gedateerde resultaten. Nooit spam.