Bases de données avec PHP

Maîtrisez l'interaction avec les bases de données MySQL en PHP : connexion, requêtes SQL, et PDO.

Introduction aux bases de données en PHP

PHP offre plusieurs moyens d'interagir avec les bases de données, particulièrement avec MySQL qui reste l'une des combinaisons les plus populaires pour le développement web. Dans ce module, nous explorerons :

Prérequis : Pour ce module, vous devez avoir un serveur MySQL/MariaDB installé (comme celui inclus dans WAMP, XAMPP ou MAMP) et des connaissances de base en SQL.

Pourquoi utiliser des bases de données ?

Les bases de données sont essentielles au développement web moderne pour plusieurs raisons :

Persistance des données

Contrairement aux variables PHP qui disparaissent à la fin de l'exécution du script, les données stockées dans une base de données restent disponibles entre les différentes requêtes et sessions.

Structuration

Les bases de données relationnelles permettent d'organiser les informations selon un modèle structuré (tables, colonnes) qui facilite leur gestion et maintient leur intégrité.

Performance

Les SGBD (Systèmes de Gestion de Bases de Données) sont optimisés pour effectuer des opérations rapides sur de grands volumes de données, grâce notamment aux index et aux requêtes optimisées.

Sécurité

Les SGBD offrent des mécanismes avancés pour sécuriser les données : gestion fine des permissions, validation de l'intégrité, journalisation des modifications, etc.

Types de SGBD courants

Il existe plusieurs types de systèmes de gestion de bases de données, chacun avec ses avantages :

Conseil : Pour la majorité des projets web PHP, la combinaison MySQL/MariaDB avec PDO offre un excellent équilibre entre simplicité, performance et fiabilité.

Connexion à une base de données MySQL

PHP propose trois principales approches pour se connecter à une base de données MySQL :

1. L'extension MySQL (obsolète)

Cette extension est obsolète depuis PHP 5.5.0 et a été retirée de PHP 7. Elle ne doit plus être utilisée.

2. L'extension MySQLi (MySQL amélioré)

MySQLi est une extension améliorée spécifique à MySQL avec une interface procédurale et orientée objet.

Connexion avec MySQLi (Orientée Objet)

// Connexion à la base de données avec MySQLi (Orientée Objet)
$servername = "localhost";
$username = "root";
$password = ""; // Généralement vide sur les installations locales
$dbname = "ma_base";

// Création de la connexion
$conn = new mysqli($servername, $username, $password, $dbname);

// Vérification de la connexion
if ($conn->connect_error) {
    die("La connexion a échoué : " . $conn->connect_error);
}

echo "Connexion réussie";

// Fermeture de la connexion en fin de script
$conn->close();

3. PDO (PHP Data Objects)

PDO est une interface d'accès aux bases de données qui offre une couche d'abstraction uniforme pour interagir avec différents types de bases de données. C'est l'approche recommandée.

Connexion avec PDO

// Connexion à la base de données avec PDO
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "ma_base";

try {
    $dsn = "mysql:host=" . $servername . ";dbname=" . $dbname . ";charset=utf8mb4";
    $options = [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false
    ];
    
    $pdo = new PDO($dsn, $username, $password, $options);
    
    echo "Connexion réussie";
} catch (PDOException $e) {
    die("Erreur de connexion : " . $e->getMessage());
}

Explication détaillée des options PDO

Comprendre les options de configuration de PDO est crucial pour une utilisation optimale :

Options de configuration PDO
Option Description Valeurs possibles
PDO::ATTR_ERRMODE Mode de gestion des erreurs
  • PDO::ERRMODE_SILENT (défaut) : codes d'erreur à récupérer manuellement
  • PDO::ERRMODE_WARNING : déclenche des E_WARNING
  • PDO::ERRMODE_EXCEPTION (recommandé) : lance des PDOException
PDO::ATTR_DEFAULT_FETCH_MODE Format de retour par défaut des données
  • PDO::FETCH_ASSOC (recommandé) : tableau associatif
  • PDO::FETCH_NUM : tableau indexé numériquement
  • PDO::FETCH_BOTH : les deux formats
  • PDO::FETCH_OBJ : objet anonyme
  • PDO::FETCH_CLASS : instance d'une classe spécifiée
PDO::ATTR_EMULATE_PREPARES Contrôle si PDO émule les requêtes préparées
  • true (défaut) : PDO émule les requêtes préparées
  • false (recommandé) : utilise les requêtes préparées natives du pilote

Avantages de PDO :

  • Compatible avec plusieurs systèmes de base de données (MySQL, PostgreSQL, SQLite, etc.)
  • Protection native contre les injections SQL via les requêtes préparées
  • Interface orientée objet moderne
  • Gestion d'erreurs efficace avec les exceptions
  • Possibilité de changer facilement de SGBD en modifiant seulement le DSN
  • Support des transactions et autres fonctionnalités avancées

DSN (Data Source Name) : La chaîne DSN varie selon le type de base de données :

  • mysql:host=localhost;dbname=ma_base;charset=utf8mb4 (pour MySQL/MariaDB)
  • pgsql:host=localhost;dbname=ma_base;user=nom;password=pass (pour PostgreSQL)
  • sqlite:/chemin/vers/fichier.sqlite (pour SQLite)

Exécuter des requêtes SQL avec PDO

Création d'une table

Création d'une table utilisateurs

CREATE TABLE utilisateurs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nom VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    mot_de_passe VARCHAR(255) NOT NULL,
    date_inscription TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Exécuter cette requête avec PDO

Créer une table avec PDO

try {
    $sql = "CREATE TABLE utilisateurs (
        id INT AUTO_INCREMENT PRIMARY KEY,
        nom VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        mot_de_passe VARCHAR(255) NOT NULL,
        date_inscription TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )";
    
    // Utilisez exec() pour les requêtes qui ne retournent pas de résultats
    $pdo->exec($sql);
    
    echo "Table utilisateurs créée avec succès";
} catch (PDOException $e) {
    echo "Erreur lors de la création de la table : " . $e->getMessage();
}

Insertion de données

Insérer des données avec des requêtes préparées

try {
    // Préparation de la requête
    $stmt = $pdo->prepare("INSERT INTO utilisateurs (nom, email, mot_de_passe) VALUES (:nom, :email, :mot_de_passe)");
    
    // Association des paramètres
    $stmt->bindParam(':nom', $nom);
    $stmt->bindParam(':email', $email);
    $stmt->bindParam(':mot_de_passe', $mot_de_passe);
    
    // Définition des valeurs et exécution
    $nom = "Jean Dupont";
    $email = "jean@exemple.com";
    $mot_de_passe = password_hash("motdepasse123", PASSWORD_DEFAULT);
    $stmt->execute();
    
    echo "Nouvel utilisateur créé avec succès";
} catch (PDOException $e) {
    echo "Erreur d'insertion : " . $e->getMessage();
}

Comprendre les injections SQL

Les injections SQL sont parmi les vulnérabilités les plus courantes dans les applications web. Elles se produisent lorsque des données non fiables sont incorporées directement dans une requête SQL.

Exemple de code vulnérable

// NE JAMAIS FAIRE CECI - Code vulnérable aux injections SQL
$username = $_POST['username']; // Imaginons que l'utilisateur entre : "' OR '1'='1"
$password = $_POST['password'];

$sql = "SELECT * FROM utilisateurs WHERE username = '" . $username . "' AND password = '" . $password . "'";
// La requête devient : SELECT * FROM utilisateurs WHERE username = '' OR '1'='1' AND password = '...'
// Ce qui retourne tous les utilisateurs car '1'='1' est toujours vrai

$result = $mysqli->query($sql); // Danger !
Code sécurisé avec requêtes préparées

// BONNE PRATIQUE - Utilisation de requêtes préparées
$username = $_POST['username']; // Même si l'utilisateur entre : "' OR '1'='1"
$password = $_POST['password'];

$stmt = $pdo->prepare("SELECT * FROM utilisateurs WHERE username = :username AND password = :password");
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->execute();

// Les données sont traitées comme des valeurs, pas comme du code SQL

Différentes méthodes d'utilisation des requêtes préparées

PDO offre plusieurs façons d'utiliser les requêtes préparées :

Méthode bindParam()

$stmt = $pdo->prepare("SELECT * FROM produits WHERE categorie = :cat AND prix < :prix");

// bindParam() lie une variable par référence - la valeur peut changer après
$categorie = "électronique";
$prix_max = 1000;

$stmt->bindParam(':cat', $categorie);
$stmt->bindParam(':prix', $prix_max, PDO::PARAM_INT); // Spécification du type

// On peut changer les valeurs avant l'exécution
$categorie = "informatique"; // Cette nouvelle valeur sera utilisée

$stmt->execute();
Méthode bindValue()

$stmt = $pdo->prepare("SELECT * FROM livres WHERE auteur = :auteur AND annee > :annee");

// bindValue() lie une valeur directement - pas affectée par les changements ultérieurs
$auteur = "Victor Hugo";
$annee_min = 1850;

$stmt->bindValue(':auteur', $auteur);
$stmt->bindValue(':annee', $annee_min, PDO::PARAM_INT);

// Changer les valeurs n'affecte pas la requête
$auteur = "Émile Zola"; // Cette nouvelle valeur NE sera PAS utilisée

$stmt->execute();
Méthode execute() avec tableau

$stmt = $pdo->prepare("INSERT INTO commandes (client_id, produit_id, quantite, prix_unitaire) 
                     VALUES (:client, :produit, :quantite, :prix)");

// Méthode plus concise sans bindParam/bindValue
$stmt->execute([
    ':client' => 42,
    ':produit' => 157,
    ':quantite' => 3,
    ':prix' => 29.99
]);

// Alternative sans les noms de paramètres (plus court)
$stmt->execute([42, 157, 3, 29.99]);

Sécurité importante : Utilisez toujours des requêtes préparées avec bindParam() ou bindValue() pour vous protéger contre les injections SQL. N'incluez jamais directement des variables dans vos requêtes SQL.

Récupération de données

Sélectionner des données avec PDO

try {
    // Préparation et exécution de la requête
    $stmt = $pdo->query("SELECT * FROM utilisateurs");
    
    // Récupération de tous les résultats
    $utilisateurs = $stmt->fetchAll();
    
    // Affichage des résultats
    if (count($utilisateurs) > 0) {
        foreach($utilisateurs as $utilisateur) {
            echo "ID: " . $utilisateur['id'] . " - Nom: " . $utilisateur[" - Email: " . $utilisateur["
"
; } } else { echo "Aucun utilisateur trouvé"; } } catch (PDOException $e) { echo "Erreur de requête : " . $e->getMessage(); }

Recherche avec des critères

Rechercher des utilisateurs par critères

try {
    $email = "jean@exemple.com";
    
    // Préparation de la requête avec condition
    $stmt = $pdo->prepare("SELECT * FROM utilisateurs WHERE email = :email");
    $stmt->bindParam(':email', $email);
    $stmt->execute();
    
    // Récupération du résultat
    $utilisateur = $stmt->fetch();
    
    if ($utilisateur) {
        echo "Utilisateur trouvé : " . $utilisateur[else {
        echo "Aucun utilisateur trouvé avec cet email";
    }
} catch (PDOException $e) {
    echo "Erreur de requête : " . $e->getMessage();
}

Mise à jour et suppression de données

Mise à jour des données

Mettre à jour un utilisateur

try {
    // Préparation de la requête de mise à jour
    $stmt = $pdo->prepare("UPDATE utilisateurs SET nom = :nom WHERE id = :id");
    
    // Association des paramètres
    $stmt->bindParam(':nom', $nom);
    $stmt->bindParam(':id', $id);
    
    // Définition des valeurs et exécution
    $nom = "Jean Martin";
    $id = 1;
    $stmt->execute();
    
    echo "Utilisateur mis à jour avec succès. Nombre de lignes affectées : " . $stmt->rowCount();
} catch (PDOException $e) {
    echo "Erreur de mise à jour : " . $e->getMessage();
}

Suppression de données

Supprimer un utilisateur

try {
    // Préparation de la requête de suppression
    $stmt = $pdo->prepare("DELETE FROM utilisateurs WHERE id = :id");
    
    // Association du paramètre
    $stmt->bindParam(':id', $id);
    
    // Définition de la valeur et exécution
    $id = 1;
    $stmt->execute();
    
    echo "Utilisateur supprimé avec succès. Nombre de lignes affectées : " . $stmt->rowCount();
} catch (PDOException $e) {
    echo "Erreur de suppression : " . $e->getMessage();
}

Transactions

Les transactions permettent d'exécuter un ensemble de requêtes comme une seule unité de travail. Si une requête échoue, toutes les modifications sont annulées (rollback).

Utiliser les transactions avec PDO

try {
    // Désactiver le mode autocommit pour gérer manuellement les transactions
    $pdo->beginTransaction();

    // Première requête
    $stmt1 = $pdo->prepare("INSERT INTO comptes (utilisateur_id, solde) VALUES (:utilisateur_id, :solde)");
    $stmt1->execute([
        ':utilisateur_id' => 1,
        ':solde' => 1000
    ]);

    // Deuxième requête
    $stmt2 = $pdo->prepare("UPDATE utilisateurs SET statut = 'actif' WHERE id = :id");
    $stmt2->execute([':id' => 1]);

    // Si tout s'est bien passé, on valide les modifications
    $pdo->commit();
    
    echo "Transactions effectuées avec succès";
} catch (Exception $e) {
    // En cas d'erreur, on annule toutes les modifications
    $pdo->rollBack();
    
    echo "Erreur : " . $e->getMessage();
}

Gestion des relations entre tables

Exemple de modèle relationnel

UTILISATEURS
🔑 id INT
nom VARCHAR(100)
email VARCHAR(100)
mot_de_passe VARCHAR(255)
ARTICLES
🔑 id INT
titre VARCHAR(200)
contenu TEXT
🔗 utilisateur_id INT
date_creation TIMESTAMP
COMMENTAIRES
🔑 id INT
texte TEXT
🔗 utilisateur_id INT
🔗 article_id INT
date_creation TIMESTAMP
🔑 Clé primaire
🔗 Clé étrangère
TIMESTAMP = Date de création

Types de relations entre tables

Les bases de données relationnelles permettent d'établir différents types de relations entre les tables :

Relation un-à-un (1:1)

Chaque enregistrement d'une table est associé à un seul enregistrement d'une autre table.

Exemple : Un utilisateur a un seul profil détaillé.


CREATE TABLE utilisateurs (
    id INT PRIMARY KEY,
    email VARCHAR(100)
);

CREATE TABLE profils (
    id INT PRIMARY KEY,
    utilisateur_id INT UNIQUE,
    adresse TEXT,
    telephone VARCHAR(20),
    FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
);
Relation un-à-plusieurs (1:N)

Un enregistrement d'une table est associé à plusieurs enregistrements d'une autre table.

Exemple : Un utilisateur peut écrire plusieurs articles.


CREATE TABLE utilisateurs (
    id INT PRIMARY KEY
);

CREATE TABLE articles (
    id INT PRIMARY KEY,
    titre VARCHAR(200),
    utilisateur_id INT,
    FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id)
);
Relation plusieurs-à-plusieurs (N:M)

Plusieurs enregistrements d'une table sont associés à plusieurs enregistrements d'une autre table.

Exemple : Des étudiants peuvent suivre plusieurs cours, et chaque cours peut être suivi par plusieurs étudiants.


CREATE TABLE etudiants (
    id INT PRIMARY KEY,
    nom VARCHAR(100)
);

CREATE TABLE cours (
    id INT PRIMARY KEY,
    intitule VARCHAR(100)
);

CREATE TABLE inscriptions (
    etudiant_id INT,
    cours_id INT,
    date_inscription DATE,
    PRIMARY KEY (etudiant_id, cours_id),
    FOREIGN KEY (etudiant_id) REFERENCES etudiants(id),
    FOREIGN KEY (cours_id) REFERENCES cours(id)
);

Requêtes avec jointures avancées

Les jointures sont essentielles pour exploiter efficacement les relations entre les tables. Voici différents types de jointures :

INNER JOIN

Retourne les enregistrements qui ont des correspondances dans les deux tables.


try {
    $stmt = $pdo->query("
        SELECT 
            a.titre, a.contenu, a.date_creation,
            u.nom AS auteur_nom
        FROM 
            articles a
        INNER JOIN 
            utilisateurs u ON a.utilisateur_id = u.id
    ");
    
    $articles = $stmt->fetchAll();
    
    foreach ($articles as $article) {
        echo "Titre: {$article['titre']} - Auteur: {$article['auteur_nom']}";
    }
} catch (PDOException $e) {
    echo "Erreur : " . $e->getMessage();
}

Visualisation : INNER JOIN ne retourne que les correspondances existantes dans les deux tables

INNER JOIN visualization
LEFT JOIN

Retourne tous les enregistrements de la table de gauche et les correspondances de la table de droite.


// Récupérer tous les utilisateurs, même ceux qui n'ont pas écrit d'articles
$stmt = $pdo->query("
    SELECT 
        u.nom, u.email,
        a.titre, a.date_creation
    FROM 
        utilisateurs u
    LEFT JOIN 
        articles a ON u.id = a.utilisateur_id
");

$resultats = $stmt->fetchAll();

foreach ($resultats as $resultat) {
    $article = $resultat['titre'] ? $resultat['titre'] : "Aucun article";
    echo "Utilisateur: {$resultat['nom']} - Article: {$article}";
}

Visualisation : LEFT JOIN inclut tous les enregistrements de la table de gauche

LEFT JOIN visualization
Jointures multiples

Récupérer des données à partir de plusieurs tables liées entre elles.


// Récupérer les articles avec leurs auteurs et leurs commentaires
$stmt = $pdo->prepare("
    SELECT 
        a.id AS article_id, a.titre, a.date_creation,
        u.nom AS auteur_nom,
        c.id AS commentaire_id, c.texte AS commentaire_texte,
        cu.nom AS commentateur_nom
    FROM 
        articles a
    INNER JOIN 
        utilisateurs u ON a.utilisateur_id = u.id
    LEFT JOIN 
        commentaires c ON a.id = c.article_id
    LEFT JOIN 
        utilisateurs cu ON c.utilisateur_id = cu.id
    WHERE 
        a.id = :article_id
    ORDER BY 
        c.date_creation DESC
");

$article_id = 5; // ID de l'article recherché
$stmt->bindParam(':article_id', $article_id, PDO::PARAM_INT);
$stmt->execute();

$resultats = $stmt->fetchAll();

// Traitement des résultats
if (count($resultats) > 0) {
    // Information sur l'article (identiques pour toutes les lignes)
    echo "

{$resultats[0]['titre']}

"
; echo "

Auteur: {$resultats[0]['auteur_nom']}

"
; // Commentaires (peuvent varier d'une ligne à l'autre) if ($resultats[0]['commentaire_id']) { echo "

Commentaires :

"
; foreach ($resultats as $resultat) { echo "
"; echo "

{$resultat['commentaire_texte']}

"
; echo "

Par: {$resultat['commentateur_nom']}

"
; echo "
"
; } } else { echo "

Aucun commentaire pour cet article.

"
; } }

Requête avec jointure

Récupérer les articles avec leurs auteurs

try {
    $stmt = $pdo->query("
        SELECT 
            a.id, a.titre, a.contenu, a.date_creation,
            u.id AS auteur_id, u.nom AS auteur_nom
        FROM 
            articles a
        INNER JOIN 
            utilisateurs u ON a.utilisateur_id = u.id
        ORDER BY 
            a.date_creation DESC
    ");
    
    $articles = $stmt->fetchAll();
    
    foreach ($articles as $article) {
        echo "

" . $article["

"
; echo "

Par : " . $article["

"
; echo "

" . $article["

"
; } } catch (PDOException $e) { echo "Erreur : " . $e->getMessage(); }

Bonnes pratiques

Sécurité des bases de données

La sécurité des bases de données va au-delà des simples requêtes préparées. Voici comment renforcer la sécurité :

Hachage des mots de passe

// JAMAIS comme ceci
$password = "motdepasse123"; // Stockage en clair ❌
$password = md5("motdepasse123"); // Hachage faible ❌

// TOUJOURS comme cela
$password_hash = password_hash("motdepasse123", PASSWORD_DEFAULT); // ✅

// Vérification
if (password_verify("motdepasse123", $password_hash)) {
    echo "Mot de passe correct!";
}
Gestion des utilisateurs de la base de données

Évitez d'utiliser l'utilisateur 'root' dans votre application.


-- Créer un utilisateur spécifique pour votre application
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';

-- Accorder uniquement les privilèges nécessaires
GRANT SELECT, INSERT, UPDATE, DELETE 
ON myapp.* 
TO 'app_user'@'localhost';

-- PAS de privilèges administratifs
-- N'accordez pas : DROP, ALTER, CREATE, etc.

Optimisation des performances

Les bases de données peuvent devenir un goulot d'étranglement pour les performances. Voici quelques techniques d'optimisation :

Création d'index adaptés

-- Ajout d'un index sur une colonne fréquemment recherchée
CREATE INDEX idx_utilisateurs_email ON utilisateurs (email);

-- Index composite pour les recherches combinées
CREATE INDEX idx_articles_date_categorie ON articles (date_creation, categorie_id);

Les index accélèrent considérablement les requêtes SELECT, mais ralentissent légèrement les INSERT/UPDATE. Utilisez-les judicieusement sur les colonnes :

  • Présentes dans les clauses WHERE
  • Utilisées pour les jointures (clés étrangères)
  • Apparaissant dans les GROUP BY ou ORDER BY
Pagination des résultats

Pour les listes longues, évitez de récupérer tous les résultats d'un coup :


$page = isset($_GET['page']) ? (int) $_GET[$limite = 10; // Nombre de résultats par page
$offset = ($page - 1) * $limite;

// Requête paginée
$stmt = $pdo->prepare("
    SELECT * FROM articles
    ORDER BY date_creation DESC
    LIMIT :limite OFFSET :offset
");

$stmt->bindParam(':limite', $limite, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

// Récupération du nombre total pour la pagination
$stmt_count = $pdo->query("SELECT COUNT(*) FROM articles");
$total = $stmt_count->fetchColumn();
$pages_totales = ceil($total / $limite);

Gestion des connexions

Utilisation d'une classe de connexion

Pour une application bien structurée, créez une classe pour gérer vos connexions à la base de données :


class Database {
    private static $instance = null;
    private $pdo;
    private $host = "localhost";
    private $db = "ma_base";
    private $user = "app_user";
    private $pass = "mot_de_passe_securise";
    
    private function __construct() {
        $dsn = "mysql:host=" . $this->host . ";dbname=" . $this->db . ";charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ];
        
        try {
            $this->pdo = new PDO($dsn, $this->user, $this->pass, $options);
        } catch (PDOException $e) {
            die("Connexion échouée : " . $e->getMessage());
        }
    }
    
    // Empêche le clonage
    private function __clone() {}
    
    // Pattern Singleton pour une seule connexion
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    // Récupérer l'objet PDO pour exécuter les requêtes
    public function getConnection() {
        return $this->pdo;
    }
}

// Utilisation
$db = Database::getInstance();
$pdo = $db->getConnection();

// Ensuite, utilisez $pdo comme d'habitude
$stmt = $pdo->prepare("SELECT * FROM utilisateurs");