TP – MySQL Triggers, Procédures stockées, Fonctions & gestion d’erreurs PHP (BTS SIO SLAM – 4 h)
Objectifs
- Mettre en œuvre un trigger BEFORE INSERT pour contrôler la disponibilité d’un livre.
- Créer une procédure stockée pour gérer le retour d’un livre.
- Créer une fonction SQL pour calculer le nombre d’emprunts actifs d’un adhérent.
- Gérer les erreurs SQL côté PHP via PDO et
try/catch(affichage d’un message explicite).
Scénario
Mini-gestion de bibliothèque : on empêche qu’un livre déjà emprunté soit réemprunté tant qu’il n’a pas été rendu. Les emprunts et retours sont tracés. Le code PHP doit remonter proprement les erreurs métier.
Étape 1 — Création de la base et des tables
Exécuter dans MySQL (par ex. via phpMyAdmin ou le client en ligne de commande) :
CREATE DATABASE IF NOT EXISTS biblio_tp
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE biblio_tp;
-- Tables
CREATE TABLE adherent (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
prenom VARCHAR(50) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE livre (
id INT AUTO_INCREMENT PRIMARY KEY,
titre VARCHAR(100) NOT NULL,
disponible BOOLEAN NOT NULL DEFAULT TRUE
) ENGINE=InnoDB;
CREATE TABLE emprunt (
id INT AUTO_INCREMENT PRIMARY KEY,
id_adherent INT NOT NULL,
id_livre INT NOT NULL,
date_emprunt DATE NOT NULL DEFAULT (CURRENT_DATE()),
date_retour DATE NULL,
CONSTRAINT fk_emprunt_adherent FOREIGN KEY (id_adherent) REFERENCES adherent(id),
CONSTRAINT fk_emprunt_livre FOREIGN KEY (id_livre) REFERENCES livre(id)
) ENGINE=InnoDB;
-- Données de test
INSERT INTO adherent (nom, prenom) VALUES
('Durand', 'Alice'),
('Martin', 'Paul');
INSERT INTO livre (titre) VALUES
('Le Meilleur des mondes'),
('1984'),
('Dune');
Exemple après l’execution de cette requête SQL : 
Étape 2 — Trigger : empêcher le double emprunt d’un même livre
Ce trigger vérifie la disponibilité dans livre et lève une erreur si le livre est déjà emprunté. S’il est disponible, il le marque comme indisponible au moment de l’insertion.
ATTENTION : Ce trigger ce met sur la table emprunt ! Et c’est un BEFORE INSERT.
DELIMITER //
CREATE TRIGGER verif_disponibilite
BEFORE INSERT ON emprunt
FOR EACH ROW
BEGIN
DECLARE v_dispo BOOLEAN;
-- Vérifie l'existence du livre
IF (SELECT COUNT(*) FROM livre WHERE id = NEW.id_livre) = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Livre inexistant.';
END IF;
-- Récupère la disponibilité
SELECT disponible INTO v_dispo FROM livre WHERE id = NEW.id_livre;
IF v_dispo = FALSE THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Le livre est déjà emprunté.';
ELSE
UPDATE livre SET disponible = FALSE WHERE id = NEW.id_livre;
END IF;
END//
DELIMITER ;
On va tester en SQL le trigger :
INSERT INTO emprunt (id_adherent, id_livre) VALUES (1, 1);
Normalement vous devriez pas avoir d’erreur.
Alice vient d’emprunter le livre le Meilleur des mondes
Vérifié que le livre n’est plus a disponible ! disponible est passé à 0.
Si c’est pas le cas revoir le code !
On test à nouveau :
INSERT INTO emprunt (id_adherent, id_livre) VALUES (2, 1);
Paul veut emprunter le même livre…
Vous devriez avoir l’erreur 1644 et le message : Le livre est déjà emprunté.
Remarque importante :
Pourquoi l’erreur 1644 apparaît avec SIGNAL ?
Quand on utilise SIGNAL SQLSTATE '45000' dans MySQL ou MariaDB,
le système renvoie par défaut le code d’erreur 1644
(ER_SIGNAL_EXCEPTION), même si on indique un autre SQLSTATE.
Le SQLSTATE ('45000', '23000'…) et le numéro d’erreur MySQL
sont deux choses différentes.
- SQLSTATE = code standard à 5 caractères (catégorie d’erreur)
- MYSQL_ERRNO = numéro d’erreur MySQL (1644 par défaut)
Le SQLSTATE pourra être récupéré en PHP par exemple facilement avec :
catch (PDOException $e)
{
echo "Erreur SQLSTATE : " . $e->getCode() . "<br>";
echo "Message : " . $e->getMessage() . "<br>";
}
Pour afficher un autre code d’erreur :
SIGNAL SQLSTATE '45000'
SET MYSQL_ERRNO = 20001,
MESSAGE_TEXT = 'Le livre est déjà emprunté.';
Exemples :
'23000'→ violation d’intégrité (ex. livre inexistant)'45000'→ erreur métier (ex. livre déjà emprunté)
En résumé : 1644 = erreur générique levée par SIGNAL.
Utilisez MYSQL_ERRNO pour personnaliser le code, et
MESSAGE_TEXT pour afficher un message clair.
Étape 3 — Procédure stockée : enregistrer le retour d’un livre
La procédure met la date de retour sur l’emprunt en cours et remet le livre disponible. Si aucun emprunt en cours n’est trouvé, elle lève une erreur.
DELIMITER //
CREATE PROCEDURE rendre_livre(IN p_id_livre INT)
BEGIN
DECLARE v_rows INT DEFAULT 0;
UPDATE emprunt
SET date_retour = CURRENT_DATE()
WHERE id_livre = p_id_livre AND date_retour IS NULL;
SET v_rows = ROW_COUNT();
IF v_rows = 0 THEN
SIGNAL SQLSTATE '46000'
SET MESSAGE_TEXT = 'Aucun emprunt en cours pour ce livre.';
ELSE
UPDATE livre SET disponible = TRUE WHERE id = p_id_livre;
END IF;
END//
DELIMITER ;
TAF :
- Tester cette procédure en simulant un retour du livre Le meilleur des mondes !
- Vérifié que la date de retour à été mise à jour et que le livre est bien à nouveau disponible.
Étape 4 — Fonction : nombre d’emprunts actifs d’un adhérent
DELIMITER //
CREATE FUNCTION nb_emprunts_actifs(p_id_adherent INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE v_nb INT;
SELECT COUNT(*) INTO v_nb
FROM emprunt
WHERE id_adherent = p_id_adherent
AND date_retour IS NULL;
RETURN v_nb;
END//
DELIMITER ;
-- Test
SELECT nb_emprunts_actifs(1) AS nb_actifs_adherent_1;
TAF :
Refaite des emprunts et vérifier que tout se passe normalement.
Étape 5 — PHP (PDO + try/catch) : tester et afficher les erreurs
Créer un fichier test_emprunt.php (adapter l’utilisateur/mot de passe MySQL si besoin). Lancer dans le navigateur ; le script tente un double emprunt pour déclencher l’erreur du trigger, puis appelle la procédure de retour.
Remarque : La classe BiblioDB joue le rôle d’un pont entre le PHP et la base de données : elle illustre la communication avec MySQL, pas encore la modélisation métier, il n’est pas nécessaire de créer des classes Livre, Emprunt, Adherent à ce stade : ce serait une complexification inutile pour une simple démonstration d’accès aux données et de gestion d’erreurs SQL.
<?php
class BiblioDB {
private PDO $pdo;
public function __construct() {
try {
$this->pdo = new PDO(
'mysql:host=localhost;dbname=biblio_tp;charset=utf8mb4',
'root', // <-- utilisateur MySQL
'', // <-- mot de passe MySQL
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
} catch (PDOException $e) {
// Erreur de connexion : on arrête tout de suite
die('Erreur de connexion : ' . htmlspecialchars($e->getMessage()));
}
}
public function emprunterLivre(int $idAdherent, int $idLivre): void {
try {
$stmt = $this->pdo->prepare(
'INSERT INTO emprunt (id_adherent, id_livre) VALUES (:a, :l)'
);
$stmt->execute([':a' => $idAdherent, ':l' => $idLivre]);
echo '<p style="color:green">Emprunt enregistré (adhérent ' . $idAdherent . ', livre ' . $idLivre . ').</p>';
} catch (PDOException $e) {
// Affiche le message levé par SIGNAL (trigger) avec SQLSTATE 45000
echo '<p style="color:#b00020"><strong>Erreur emprunt</strong> : ' . htmlspecialchars($e->getMessage()) . '</p>';
// Pour un traitement fin, on peut aussi exploiter $e->getCode() (SQLSTATE)
// echo '<p>SQLSTATE : ' . htmlspecialchars($e->getCode()) . '</p>';
}
}
public function rendreLivre(int $idLivre): void {
try {
$this->pdo->exec('CALL rendre_livre(' . (int)$idLivre . ')');
echo '<p style="color:green">Livre ' . $idLivre . ' rendu avec succès.</p>';
} catch (PDOException $e) {
echo '<p style="color:#b00020"><strong>Erreur retour</strong> : ' . htmlspecialchars($e->getMessage()) . '</p>';
}
}
public function nbEmpruntsActifs(int $idAdherent): void {
$stmt = $this->pdo->query('SELECT nb_emprunts_actifs(' . (int)$idAdherent . ') AS nb');
$row = $stmt->fetch();
echo '<p>Emprunts actifs de l’adhérent ' . $idAdherent . ' : <strong>' . (int)$row['nb'] . '</strong></p>';
}
}
/* ---------- Démo contrôlée ---------- */
$db = new BiblioDB();
echo '<h3>Démo</h3>';
/* 1) Emprunt du livre 1 par l’adhérent 1 (OK) */
$db->emprunterLivre(1, 1);
/* 2) Tentative de réemprunter le même livre (déclenche le trigger => erreur) */
$db->emprunterLivre(2, 1);
/* 3) Statistiques adhérent 1 */
$db->nbEmpruntsActifs(1);
/* 4) Retour du livre 1 (procédure stockée) */
$db->rendreLivre(1);
/* 5) Ré-emprunt possible après retour */
$db->emprunterLivre(2, 1);
?>
Questions de validation (rapides)
- Que se passe-t-il si on insère un emprunt pour un livre dont
disponible = FALSE? Quelle est la SQLSTATE remontée ? - Que se passe-t-il si on appelle
rendre_livresur un livre qui n’a aucun emprunt en cours ? - Expliquez l’intérêt de lever l’erreur côté MySQL (trigger) plutôt que de ne gérer la règle qu’en PHP.
TAF
- Ajouter une contrainte d’âge d’emprunt maximum (ex. 30 jours) et une fonction qui calcule le nombre de jours de retard.
- Enrichir le trigger pour interdire un nouvel emprunt si l’adhérent a déjà N emprunts actifs (utiliser la fonction créée).
- Journaliser les erreurs métier dans une table
journal_erreurs(horodatage + message) via un second triggerAFTER INSERTsuremprunten cas d’échec simulé (à discuter en classe).
Ce contenu est réservé aux membres du site. Si vous êtes un utilisateur existant, veuillez vous connecter. Les nouveaux utilisateurs peuvent s'inscrire ci-dessous.
