MODULE 07

MySQL & PDO

Bases de donnΓ©es relationnelles avec PHP Data Objects : connexion, CRUD, requΓͺtes prΓ©parΓ©es et transactions.

1 β€” Connexion PDO

<?php
// ── Connexion sΓ©curisΓ©e ───────────────────────────────────
// Les credentials doivent venir de variables d'environnement, jamais en dur
$dsn = 'mysql:host=localhost;dbname=formation_php;charset=utf8mb4';
// utf8mb4 : support des emoji et des caractères Unicode complets

try {
    $pdo = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS'], [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // lève des exceptions en cas d'erreur SQL
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // tableaux associatifs par dΓ©faut
        PDO::ATTR_EMULATE_PREPARES   => false,                  // dΓ©sactiver l'Γ©mulation (vraies requΓͺtes prΓ©parΓ©es)
    ]);
} catch (PDOException $e) {
    // Ne jamais afficher le message d'erreur brut en production
    error_log($e->getMessage()); // journaliser cΓ΄tΓ© serveur
    die("Erreur de connexion."); // message gΓ©nΓ©rique cΓ΄tΓ© client
}

// ── Bonnes pratiques config ───────────────────────────────
// En prod : stocker dans .env (jamais dans Git)
// DB_HOST=localhost
// DB_NAME=formation_php
// DB_USER=app_user
// DB_PASS=motdepasse_fort

2 β€” CRUD de base

-- SchΓ©ma de la table articles (Γ  crΓ©er une seule fois)
CREATE TABLE articles (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    titre      VARCHAR(200) NOT NULL,
    contenu    TEXT NOT NULL,
    auteur     VARCHAR(100) NOT NULL DEFAULT 'Anonyme',
    publie     TINYINT(1) NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
<?php
// ── CREATE (INSERT) ───────────────────────────────────────
$stmt = $pdo->prepare(
    'INSERT INTO articles (titre, contenu, auteur, publie) VALUES (:titre, :contenu, :auteur, :publie)'
);
$stmt->execute([
    ':titre'   => 'Premier article',
    ':contenu' => 'Contenu de l\'article...',
    ':auteur'  => 'Alice',
    ':publie'  => 1,
]);
$newId = $pdo->lastInsertId(); // ID auto-gΓ©nΓ©rΓ©

// ── READ (SELECT) ─────────────────────────────────────────
// Tous les articles publiΓ©s, triΓ©s par date
$stmt = $pdo->query('SELECT id, titre, auteur, created_at FROM articles WHERE publie = 1 ORDER BY created_at DESC');
$articles = $stmt->fetchAll(); // tableau de tous les rΓ©sultats

// ── UPDATE ────────────────────────────────────────────────
$stmt = $pdo->prepare('UPDATE articles SET titre = :titre, publie = :publie WHERE id = :id');
$stmt->execute([':titre' => 'Nouveau titre', ':publie' => 1, ':id' => 5]);
$affectes = $stmt->rowCount(); // nombre de lignes modifiΓ©es

// ── DELETE ────────────────────────────────────────────────
$stmt = $pdo->prepare('DELETE FROM articles WHERE id = :id');
$stmt->execute([':id' => 5]);
echo $stmt->rowCount() > 0 ? 'SupprimΓ©' : 'Non trouvΓ©';

3 β€” RequΓͺtes prΓ©parΓ©es & Injection SQL

<?php
// ── Injection SQL β€” DANGER ────────────────────────────────
// NE JAMAIS faire Γ§a :
$id = $_GET['id']; // pourrait valoir "1 OR 1=1"
// $pdo->query("SELECT * FROM users WHERE id = $id"); // vulnΓ©rable !

// ── RequΓͺte prΓ©parΓ©e β€” CORRECT ────────────────────────────
// Le placeholder :id est traité comme une DONNÉE, jamais comme du SQL
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = :id AND publie = :publie');
$stmt->bindParam(':id',     $_GET['id'], PDO::PARAM_INT); // typage strict
$stmt->bindParam(':publie', $publie,     PDO::PARAM_BOOL);
$publie = true;
$stmt->execute();
$article = $stmt->fetch(); // un seul rΓ©sultat

// ── Syntaxe courte (execute direct) ───────────────────────
$stmt = $pdo->prepare('SELECT * FROM articles WHERE auteur = ? AND publie = ?');
$stmt->execute(['Alice', 1]); // positionnels (?)
$articles = $stmt->fetchAll();

// ── Recherche avec LIKE ───────────────────────────────────
$recherche = '%' . addcslashes($_GET['q'] ?? '', '%_') . '%'; // Γ©chapper % et _
$stmt = $pdo->prepare('SELECT * FROM articles WHERE titre LIKE :q OR contenu LIKE :q');
$stmt->execute([':q' => $recherche]);
$resultats = $stmt->fetchAll();
β›” Injection SQL : ne jamais interpoler de variables directement dans une requΓͺte SQL. Toujours utiliser des requΓͺtes prΓ©parΓ©es avec des paramΓ¨tres liΓ©s.

4 β€” Modes de rΓ©cupΓ©ration (fetch)

<?php
// ── FETCH_ASSOC (dΓ©faut si configurΓ©) ─────────────────────
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// ['id' => 1, 'titre' => '...', ...]

// ── FETCH_OBJ : objet stdClass ────────────────────────────
$row = $stmt->fetch(PDO::FETCH_OBJ);
echo $row->titre; // accès propriété

// ── FETCH_CLASS : mapper vers une classe personnalisΓ©e ─────
class Article {
    public int    $id;
    public string $titre;
    public string $contenu;
    public string $auteur;
    public bool   $publie;
}
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = :id');
$stmt->execute([':id' => 1]);
$stmt->setFetchMode(PDO::FETCH_CLASS, Article::class);
$article = $stmt->fetch(); // instance de Article avec propriΓ©tΓ©s remplies

// ── FETCH_COLUMN : une seule colonne ─────────────────────
$titres = $pdo->query('SELECT titre FROM articles')
               ->fetchAll(PDO::FETCH_COLUMN); // ['Titre 1', 'Titre 2', ...]

// ── FETCH_KEY_PAIR : tableau clΓ© => valeur ────────────────
$map = $pdo->query('SELECT id, titre FROM articles')
            ->fetchAll(PDO::FETCH_KEY_PAIR); // [1 => 'Titre 1', 2 => 'Titre 2']

5 β€” Transactions

<?php
// Transactions : garantit que toutes les opΓ©rations rΓ©ussissent ou aucune
// PropriΓ©tΓ©s ACID : AtomicitΓ©, CohΓ©rence, Isolation, DurabilitΓ©
try {
    $pdo->beginTransaction();

    // Exemple : virement bancaire β€” DEUX opΓ©rations INDIVISIBLES
    $stmt1 = $pdo->prepare('UPDATE comptes SET solde = solde - :montant WHERE id = :id');
    $stmt1->execute([':montant' => 100, ':id' => 1]);

    $stmt2 = $pdo->prepare('UPDATE comptes SET solde = solde + :montant WHERE id = :id');
    $stmt2->execute([':montant' => 100, ':id' => 2]);

    // VΓ©rification de cohΓ©rence avant commit
    $solde = $pdo->query('SELECT solde FROM comptes WHERE id = 1')->fetchColumn();
    if ($solde < 0) {
        throw new RuntimeException("Solde insuffisant");
    }

    $pdo->commit();   // valider : les deux updates sont permanents
} catch (Exception $e) {
    $pdo->rollBack(); // annuler : aucune des deux modifications n'est appliquΓ©e
    error_log($e->getMessage());
    echo "Transaction annulΓ©e.";
}

6 β€” Bonnes pratiques schΓ©ma SQL

-- ── Users (authentification) ─────────────────────────────
CREATE TABLE users (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username     VARCHAR(60) NOT NULL UNIQUE,
    email        VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,       -- stockΓ© avec password_hash()
    role         ENUM('user','admin') DEFAULT 'user',
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_email (email)                    -- index pour les recherches par email
);

-- ── Articles (clΓ© Γ©trangΓ¨re) ──────────────────────────────
CREATE TABLE articles (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id    INT UNSIGNED NOT NULL,
    titre      VARCHAR(200) NOT NULL,
    slug       VARCHAR(220) NOT NULL UNIQUE,   -- URL-friendly
    contenu    TEXT NOT NULL,
    publie     TINYINT(1) DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_publie (publie),
    INDEX idx_slug (slug)
);