Tutoriel
Partie 19 — PHP & MySQL : connexion sécurisée avec PDO

Partie 19 — PHP & MySQL : connexion sécurisée avec PDO

Apprends PDO en PHP : connexion MySQL sécurisée, DSN, options, try/catch, requêtes préparées anti SQL injection, fetch, insert/update, pagination, transactions.

PHP 65 Mis à jour 10 hours ago
Conseil : lisez d’abord les sections clés, puis essayez un QCM lié à la même notion pour valider votre compréhension.

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 proprement
  • EMULATE_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 PDO
  • UserRepository.php (ou functions) : requêtes liées aux users
  • PostRepository.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)

  1. Créer une page users.php qui liste les 20 derniers users (PDO fetchAll).
  2. Créer un formulaire “Ajouter user” + insert sécurisé.
  3. Créer une page “Détail user” via ?id= + SELECT préparé.
  4. Créer une pagination “posts” (LIMIT/OFFSET).
  5. 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).