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=utf8mb4a 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$sortdans le SQL. LIMIT ?avec des requêtes préparées émulées. AvecATTR_EMULATE_PREPARESpar défaut sur MySQL,execute(['10'])envoieLIMIT '10', une erreur de syntaxe. Liez-le avecPDO::PARAM_INTou désactivez l'émulation.- Le même marqueur nommé deux fois.
WHERE first = :q OR last = :qne fonctionne qu'avec des requêtes préparées émulées. Avec de vraies requêtes préparées, vous obtenezSQLSTATE[HY093]: Invalid parameter number; utilisez:q1et:q2. - Oublier que
fetch()renvoiefalse.$user['name']surfalsedonne un warning etnull; 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.