Menu

PHP PDO : se connecter à MySQL et requêtes préparées

PDO est la couche d'accès aux bases de données de PHP : connectez-vous avec new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass), puis exécutez chaque requête contenant des valeurs avec prepare() et execute([$value]). Connexion, requêtes préparées, lecture des lignes, insertions, transactions et WHERE IN.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

PDO (PHP Data Objects) est la façon native de dialoguer avec une base de données. Connectez-vous avec new PDO($dsn, $user, $password), puis exécutez toute requête contenant une valeur avec prepare() et execute(), pour que la valeur soit envoyée séparément du SQL :

<?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

Les exemples de cette page ont besoin d'un serveur de base de données, ils sont donc présentés comme du code simple. L'exemple d'injection SQL plus bas s'exécute.

Se connecter à MySQL avec PDO

Le premier argument est un DSN : le nom du pilote, puis les détails de connexion. Placez la connexion à un seul endroit et incluez-la là où vous en avez besoin.

<?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 a sa place dans le DSN. Sans lui, les accents et les emoji peuvent arriver sous forme de ? ou d'octets déformés.
  • N'affichez jamais $e->getMessage() aux visiteurs : il peut contenir l'hôte et le nom d'utilisateur. Journalisez-le.
  • Gardez le mot de passe hors du code, dans une variable d'environnement ou un fichier de configuration hors de la racine web.

Les autres bases de données ne changent que le DSN : pgsql:host=localhost;dbname=shop pour PostgreSQL, sqlite:/var/data/app.db pour un fichier SQLite. Chacune a besoin de son pilote PDO installé (pdo_mysql, pdo_pgsql, pdo_sqlite) ; php -m liste ceux que vous avez.

Requêtes préparées avec ? et :name

Un marqueur indique où va une valeur. Utilisez des marqueurs positionnels ? avec une liste, ou des marqueurs nommés avec un tableau associatif :

<?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();

Ne mettez pas de guillemets autour d'un marqueur : WHERE name = '?' compare avec le texte littéral ?. Une requête préparée peut aussi être exécutée plusieurs fois avec des valeurs différentes, préparez-la donc une fois avant une boucle, pas à l'intérieur.

Pourquoi la concaténation de chaînes en SQL est dangereuse

C'est le seul bloc de la page qui s'exécute : il construit la requête de la mauvaise façon et de la bonne façon pour la même saisie. Modifiez $email et relancez.

Dans la requête concaténée, le guillemet de la saisie ferme la chaîne, et OR '1'='1' rend la condition vraie pour chaque ligne : la requête renvoie tous les utilisateurs. Sur un formulaire de connexion, cela permet de se connecter sans connaître le mot de passe, et si le pilote accepte plusieurs instructions en un appel, une saisie comme '; DROP TABLE users; -- exécute une seconde instruction choisie par l'attaquant. Dans la version préparée, la base de données reçoit le SQL et la valeur séparément, donc le guillemet n'est qu'un caractère dans un email qui ne correspond à personne.

Les fonctions d'échappement comme addslashes() ne sont pas une solution : elles ratent des cas qui dépendent du jeu de caractères de la connexion. Utilisez des marqueurs pour chaque valeur, y compris les nombres et les valeurs qui « viennent de votre propre code ».

Lire des lignes : fetch, fetchAll et 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() convient quand le SQL ne contient aucune valeur venant de l'extérieur. Dès qu'une variable intervient, passez à prepare().

Insérer, mettre à jour et supprimer

<?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() renvoie une chaîne, convertissez-la donc. Avec MySQL, rowCount() après un UPDATE compte les lignes réellement modifiées, donc remettre un nom à la valeur qu'il a déjà donne 0. Stockez les mots de passe avec password_hash, jamais en clair.

Transactions

Une transaction fait réussir ou échouer ensemble plusieurs instructions. Si quoi que ce soit lève une exception, annulez et rien n'est enregistré :

<?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;
}

Cela suppose que les erreurs lèvent des exceptions (le comportement par défaut depuis PHP 8.0), si bien qu'une instruction en échec saute au catch. Avec MySQL, les tables doivent utiliser InnoDB ; MyISAM ignore les transactions. La page try/catch traite de Throwable et de la relance d'exceptions.

WHERE IN avec un tableau

Un ? ne contient qu'une seule valeur, donc IN (?) avec un tableau ne fonctionne pas. Construisez un marqueur par valeur :

<?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
}

La chaîne insérée dans le SQL ne contient que des points d'interrogation et des virgules, donc la requête reste sûre.

Erreurs fréquentes avec les marqueurs

  • Noms de tables et de colonnes. ORDER BY ? trie selon une chaîne constante, pas selon la colonne. Choisissez les identifiants dans une liste autorisée : $sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, puis mettez $sort dans le SQL.
  • LIMIT ? avec des requêtes préparées émulées. Avec ATTR_EMULATE_PREPARES par défaut sur MySQL, execute(['10']) envoie LIMIT '10', une erreur de syntaxe. Liez-le avec PDO::PARAM_INT ou désactivez l'émulation.
  • Le même marqueur nommé deux fois. WHERE first = :q OR last = :q ne fonctionne qu'avec des requêtes préparées émulées. Avec de vraies requêtes préparées, vous obtenez SQLSTATE[HY093]: Invalid parameter number ; utilisez :q1 et :q2.
  • Oublier que fetch() renvoie false. $user['name'] sur false donne un warning et null ; vérifiez d'abord le résultat.

Questions fréquentes

Comment se connecter à MySQL avec PDO ?

Créez un objet PDO avec un DSN, un utilisateur et un mot de passe : $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Une connexion qui échoue lève une PDOException ; interceptez-la, journalisez le message et montrez au visiteur une erreur générique.

Les requêtes préparées empêchent-elles l'injection SQL ?

Oui, pour les valeurs : avec prepare('... WHERE id = ?') et execute([$id]), la valeur est envoyée comme une donnée et ne peut jamais modifier la requête. Les marqueurs ne peuvent pas remplacer des noms de tables, des noms de colonnes ou ASC/DESC, vérifiez donc ceux-là par rapport à une liste de noms autorisés avant de les mettre dans le SQL.

Quelle est la différence entre fetch et fetchAll dans PDO ?

fetch() renvoie la ligne suivante (ou false quand il n'y en a plus), elle convient donc à une seule ligne ou à une boucle sur un gros résultat. fetchAll() renvoie toutes les lignes d'un coup dans un tableau. fetchColumn() renvoie une valeur de la ligne suivante, pratique pour SELECT COUNT(*).

Comment utiliser WHERE IN avec un tableau dans PDO ?

Construisez un ? par valeur et passez le tableau à execute() : $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Vérifiez d'abord que le tableau n'est pas vide, car IN () est une erreur de syntaxe.

Faut-il utiliser PDO ou mysqli ?

Utilisez PDO sauf si vous avez besoin d'une fonction propre à MySQL. Son API est la même pour MySQL, PostgreSQL, SQLite et d'autres, il prend en charge les marqueurs nommés et, depuis PHP 8.0, il lève des exceptions en cas d'erreur par défaut. mysqli ne fonctionne qu'avec MySQL et MariaDB.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER