PHP PDO est la manière la plus sûre et la plus souple de dialoguer avec une base de données dans les applications PHP modernes. Utilisé correctement, il élimine quasiment l'injection SQL, l'une des attaques les plus courantes et les plus dangereuses contre les applications web. Dans ce guide, tu apprendras à ouvrir une connexion PDO, à comprendre le fonctionnement réel des requêtes préparées et à maîtriser les modèles pratiques utiles au quotidien.
Pourquoi l'injection SQL est-elle si dangereuse ?
L'injection SQL survient lorsque des données fournies par l'utilisateur sont collées directement dans une requête SQL. Au lieu d'une valeur normale, l'attaquant envoie un texte qui modifie le sens de la requête. Voici l'exemple classique et défaillant :
// NE FAIS JAMAIS CECI
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $pdo->query($sql);
Si l'utilisateur saisit ' OR '1'='1 dans le champ email, la condition est toujours vraie et tous les enregistrements peuvent fuiter. Pire encore, un attaquant pourrait supprimer des tables ou élever ses privilèges. La solution est simple : ne mélange jamais les données directement dans le texte SQL.
Configurer correctement la connexion PDO
Une bonne connexion PDO inclut quelques réglages qui empêchent les erreurs d'être avalées silencieusement et rendent le comportement prévisible. Encadre toujours la connexion dans un bloc try/catch et lis les identifiants depuis des variables d'environnement plutôt que de les coder en dur.
$dsn = 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4';
$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, $user, $pass, $options);
} catch (PDOException $e) {
// Journalise l'erreur, n'affiche jamais le message brut à l'utilisateur
error_log($e->getMessage());
exit('Connexion à la base de données impossible.');
}
Trois réglages comptent : ERRMODE_EXCEPTION lève les erreurs sous forme d'exceptions au lieu d'échouer en silence, FETCH_ASSOC renvoie des tableaux associatifs lisibles, et EMULATE_PREPARES = false force le pilote à utiliser de véritables requêtes préparées, ce qui est préférable pour la sécurité comme pour la justesse des types.
Le fonctionnement des requêtes préparées
Une requête préparée sépare la structure d'une requête de ses données. Tu prépares d'abord le modèle de requête avec des marqueurs, puis tu envoies les valeurs séparément. Ainsi, les données, aussi dangereuses qu'elles paraissent, ne sont jamais interprétées comme une commande SQL ; elles sont toujours traitées comme une valeur.
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
Ici, ? est un marqueur. Le tableau passé à execute() est lié de façon sûre aux marqueurs. Le pilote traite la valeur strictement comme une donnée et la possibilité d'injection disparaît.
Les paramètres nommés
Quand une requête comporte plusieurs paramètres, les marqueurs nommés rendent le code bien plus lisible que les ? positionnels. Les paramètres nommés commencent par deux-points :
$stmt = $pdo->prepare(
'INSERT INTO posts (title, body, user_id) VALUES (:title, :body, :user_id)'
);
$stmt->execute([
':title' => $title,
':body' => $body,
':user_id' => $userId,
]);
Pour plus de contrôle, bindValue() permet de définir le type de chaque paramètre :
$stmt = $pdo->prepare('SELECT * FROM products WHERE stock >= :min');
$stmt->bindValue(':min', $min, PDO::PARAM_INT);
$stmt->execute();
Lire les résultats
PDO propose plusieurs façons de lire les résultats. Utilise fetch() pour une seule ligne et fetchAll() pour toutes les lignes. Pour traiter un grand nombre de lignes, l'approche économe en mémoire consiste à les récupérer une par une dans une boucle :
$stmt = $pdo->prepare('SELECT id, title FROM posts WHERE user_id = ?');
$stmt->execute([$userId]);
foreach ($stmt as $row) {
echo htmlspecialchars($row['title']) . "\n";
}
Attention : utilise htmlspecialchars() lorsque tu affiches des données provenant de la base. PDO te protège de l'injection SQL, mais le XSS (cross-site scripting) est un sujet distinct qui doit être traité côté sortie.
Erreurs fréquentes
- Confondre un nom de table ou de colonne avec un paramètre. Les marqueurs ne servent qu'aux valeurs ;
ORDER BY ?ne fonctionnera pas. Valide les noms de colonnes et de tables avec une liste blanche fixe. - Laisser EMULATE_PREPARES activé. Activé, PDO assemble la requête de son côté ; c'est sûr dans la plupart des cas, mais les vraies requêtes préparées sont toujours plus robustes.
- Montrer les erreurs à l'utilisateur. Les messages d'erreur bruts révèlent la structure de ta base. Journalise-les et renvoie un message générique.
- Re-préparer la même requête dans une boucle. Appelle
prepare()une fois, puis seulementexecute()dans la boucle.
Les transactions
Quand tu effectues plusieurs écritures interdépendantes, utilise une transaction pour qu'elles réussissent toutes ou échouent toutes ensemble. Si une étape échoue, rollBack() annule tout :
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
->execute([$amount, $fromId]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
->execute([$amount, $toId]);
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
throw $e;
}
Questions fréquentes
Faut-il utiliser PDO ou MySQLi ?
Les deux gèrent les requêtes préparées et sont sûrs. L'avantage de PDO est qu'il prend en charge une douzaine de pilotes de bases de données via une interface unique et offre une API plus lisible, avec les paramètres nommés. Pour la plupart des nouveaux projets, PDO est le choix le plus souple.
Les requêtes préparées nuisent-elles aux performances ?
Non, c'est généralement l'inverse. Quand la même requête s'exécute plusieurs fois avec des valeurs différentes, la base peut réutiliser son plan d'exécution. Pour les requêtes uniques, la différence est négligeable, et la sécurité gagnée vaut bien plus que ce coût.
Les requêtes préparées suffisent-elles à elles seules ?
Contre l'injection SQL, oui, bien utilisées elles suffisent. Mais la sécurité est globale : il faut aussi une protection XSS en sortie, la validation des entrées, l'autorisation et le principe du moindre privilège.
Prends la sécurité de ta base de données au sérieux. Si tu veux que j'audite un projet PHP existant ou que je construise une couche de données sûre pour toi, contacte-moi et posons ensemble des fondations solides.