Partie 19 — PHP & MySQL : connexion sécurisée avec PDO
Maintenant que tu comprends MySQL/SQL, l’étape logique est de relier ça à PHP. Et pas juste “faire une connexion qui marche”, mais une connexion propre, sécurisée, et maintenable.
En PHP moderne, la meilleure approche générale pour MySQL est PDO (PHP Data Objects). Pourquoi ? Parce que PDO te donne :
- une API uniforme
- des requêtes préparées (protection contre SQL injection)
- une gestion d’erreurs propre via exceptions
- un code plus lisible et réutilisable
👉 Objectif : te donner un “socle PDO” que tu peux copier dans tes projets (auth, QCM, concours, articles…), et ensuite ajouter tes requêtes sans stress.
1) PDO vs MySQLi : pourquoi PDO est souvent un meilleur choix
MySQLi fonctionne, mais PDO est souvent préféré en pédagogie et en projets, car il est plus “standard” et plus extensible.
| Critère | PDO | MySQLi |
|---|---|---|
| Requêtes préparées | ✅ Oui | ✅ Oui |
| Support multi-DB | ✅ (MySQL, PostgreSQL...) | ❌ MySQL seulement |
| Style | Objet (souvent + clean) | Procédural ou objet |
| Retour des résultats | Flex (FETCH_ASSOC...) | Bon aussi |
Pour un cours orienté “bonnes pratiques”, PDO est un excellent choix.
2) La connexion PDO : DSN, charset, options (propre)
Une connexion PDO, c’est :
- un DSN (host, dbname, charset)
- un user/password
- des options (mode d’erreur, fetch mode, etc.)
2.1 Exemple connexion “pro”
<?php
// db.php
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$host = "127.0.0.1";
$db = "myapp";
$user = "root";
$pass = "";
$charset = "utf8mb4";
$dsn = "mysql:host={$host};dbname={$db};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // erreurs => exceptions
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // fetch associatif par défaut
PDO::ATTR_EMULATE_PREPARES => false, // prépare côté MySQL
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
return $pdo;
} catch (PDOException $e) {
// En DEV tu peux afficher, en PROD tu logs
error_log("DB Connection Error: " . $e->getMessage());
http_response_code(500);
die("Erreur serveur (connexion DB).");
}
}
?>
Points importants :
- utf8mb4 : support complet (emojis, caractères arabes, etc.)
ERRMODE_EXCEPTION: tu gères les erreurs proprementEMULATE_PREPARES = false: vraies requêtes préparées côté serveur- static $pdo : singleton simple (une connexion réutilisée)
⚠️ Ne laisse pas tes identifiants DB en dur en production. Utilise variables d’environnement (.env) ou config sécurisée.
3) Première requête : SELECT avec query()
Pour une requête simple sans paramètres, tu peux utiliser query().
Exemple : récupérer les 10 derniers utilisateurs.
<?php
require "db.php";
$rows = db()->query("SELECT id, name, email FROM users ORDER BY id DESC LIMIT 10")
->fetchAll();
echo "<pre>";
print_r($rows);
echo "</pre>";
?>
Mais dès que tu as des valeurs venant de l’utilisateur : prepare() obligatoire.
4) Requêtes préparées : la base de la sécurité (anti SQL Injection)
La SQL injection arrive quand tu concatènes des entrées user dans une requête. Exemple mauvais :
-- ❌ Mauvais (danger) SELECT * FROM users WHERE email = '$email';
Solution : requêtes préparées + paramètres.
4.1 SELECT sécurisé avec prepare/execute
<?php
require "db.php";
$email = $_GET["email"] ?? "";
$sql = "SELECT id, name, email FROM users WHERE email = :email LIMIT 1";
$stmt = db()->prepare($sql);
$stmt->execute(["email" => $email]);
$user = $stmt->fetch(); // assoc ou false
if (!$user) {
echo "Utilisateur introuvable";
} else {
echo "Bonjour " . htmlspecialchars($user["name"]);
}
?>
✅ Avec PDO, le paramètre n’est jamais interprété comme du SQL. Il est traité comme une valeur, donc injection bloquée.
4.2 bindValue vs execute(array)
Deux styles corrects :
<?php
$stmt = db()->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindValue(":id", 10, PDO::PARAM_INT);
$stmt->execute();
$user = $stmt->fetch();
?>
En pratique, execute([...]) suffit dans la majorité des cas.
bindValue est utile quand tu veux forcer le type (int, bool).
5) INSERT : ajouter des données + récupérer l’ID
Exemple : créer un utilisateur (name/email). On utilise une requête préparée.
<?php
require "db.php";
$name = $_POST["name"] ?? "";
$email = $_POST["email"] ?? "";
$sql = "INSERT INTO users (name, email, created_at) VALUES (:name, :email, NOW())";
$stmt = db()->prepare($sql);
$stmt->execute([
"name" => $name,
"email" => $email,
]);
$newId = db()->lastInsertId();
echo "Utilisateur créé. ID = " . (int)$newId;
?>
⚠️ Toujours valider côté PHP (email format, champs vides) avant d’insérer. PDO sécurise l’injection, pas la qualité des données.
6) UPDATE / DELETE : modifier et supprimer proprement
6.1 UPDATE sécurisé
<?php require "db.php"; $id = (int)($_POST["id"] ?? 0); $newName = $_POST["name"] ?? ""; $sql = "UPDATE users SET name = :name WHERE id = :id"; $stmt = db()->prepare($sql); $stmt->execute(["name" => $newName, "id" => $id]); echo "Lignes modifiées : " . $stmt->rowCount(); ?>
6.2 DELETE sécurisé
<?php require "db.php"; $id = (int)($_POST["id"] ?? 0); $sql = "DELETE FROM users WHERE id = :id"; $stmt = db()->prepare($sql); $stmt->execute(["id" => $id]); echo "Supprimé : " . $stmt->rowCount(); ?>
Astuce : rowCount() te dit combien de lignes ont été touchées.
Très utile pour vérifier si l’ID existait réellement.
7) Pagination : LIMIT + OFFSET (cas réel)
Sur un site de QCM/articles, tu as souvent des listes paginées. Exemple : page 1 = 20 résultats, page 2 = 20 résultats, etc.
<?php
require "db.php";
$page = max(1, (int)($_GET["page"] ?? 1));
$perPage = 20;
$offset = ($page - 1) * $perPage;
// LIMIT/OFFSET doivent être des INT : on force le type
$sql = "SELECT id, title, created_at FROM posts ORDER BY id DESC LIMIT :lim OFFSET :off";
$stmt = db()->prepare($sql);
$stmt->bindValue(":lim", $perPage, PDO::PARAM_INT);
$stmt->bindValue(":off", $offset, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll();
?>
✅ Note : pour LIMIT/OFFSET, bindValue avec PDO::PARAM_INT
évite des soucis de type.
8) Transactions : garantir la cohérence (paiement, commandes, etc.)
Une transaction, c’est : “soit tout passe, soit rien ne passe”. Exemple : créer une commande + enregistrer ses lignes. Si une étape échoue, on annule.
<?php
require "db.php";
$pdo = db();
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO orders (user_id, created_at) VALUES (:uid, NOW())");
$stmt->execute(["uid" => 1]);
$orderId = (int)$pdo->lastInsertId();
$stmt2 = $pdo->prepare("INSERT INTO order_items (order_id, product_id, qty) VALUES (:oid, :pid, :qty)");
$stmt2->execute(["oid" => $orderId, "pid" => 10, "qty" => 2]);
$pdo->commit();
echo "Commande OK (ID $orderId)";
} catch (Throwable $e) {
$pdo->rollBack();
error_log("Transaction Error: " . $e->getMessage());
die("Erreur commande.");
}
?>
Cas réel : paiements, stock, écritures comptables, import de données.
9) Structure propre : séparer la DB et les requêtes
Au début, tu peux écrire tout dans un fichier. Mais très vite, tu veux organiser :
db.php: connexion PDOUserRepository.php(ou functions) : requêtes liées aux usersPostRepository.php: requêtes posts
Exemple simple “fonction repository” :
<?php
// user_repo.php
require_once "db.php";
function findUserByEmail(string $email): ?array
{
$stmt = db()->prepare("SELECT id, name, email FROM users WHERE email = :email LIMIT 1");
$stmt->execute(["email" => $email]);
$row = $stmt->fetch();
return $row ?: null;
}
?>
Avantage : tu réutilises, tu testes plus facilement, et ton code devient “propre”.
10) Les erreurs PDO courantes (et comment les lire)
- Access denied : mauvais user/pass ou droits insuffisants
- Unknown database : nom de DB incorrect
- Table doesn't exist : migration non faite / mauvais schema
- Duplicate entry : contrainte UNIQUE (email) violée
- SQLSTATE[HY093] : paramètres manquants/mal nommés
✅ Méthode : lis le message, vérifie ta requête SQL, puis vérifie les paramètres envoyés à execute().
11) Mini-exercices (niveau réel)
- Créer une page
users.phpqui liste les 20 derniers users (PDO fetchAll). - Créer un formulaire “Ajouter user” + insert sécurisé.
- Créer une page “Détail user” via
?id=+ SELECT préparé. - Créer une pagination “posts” (LIMIT/OFFSET).
- Créer une transaction qui crée un post + un log dans une autre table.
Conclusion
PDO est le pont solide entre PHP et MySQL. Avec une connexion propre, des options correctes, et des requêtes préparées, tu évites la majorité des problèmes (SQL injection, erreurs cachées, code difficile à maintenir).
Partie 20 logique : CRUD complet (users/posts) avec PDO + validations + messages flash, ou bien sécurité SQL (transactions, index, requêtes avancées).