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)
);