Objectif
L’ORM (Object Relational Mapping) est une technique qui permet de faire le lien entre les objets et les lignes dans les tables d’une base de données. (ou fichiers : JSON, XML, etc.)
L’idée :
- Chaque ligne de table ↔️ un objet PHP
- Chaque colonne ↔️ un attribut de l’objet
On va créer un petit ORM manuel en utilisant PDO
avec try / catch pour gérer les erreurs.
Principes clés
- Hydratation : remplir automatiquement les attributs d’un objet à partir d’un tableau (résultat SQL).
- Manager : classe qui contient les méthodes pour interagir avec la base (
SELECT / INSERT / UPDATE
…). - Exceptions : permettent d’attraper les erreurs (connexion, requêtes) pour éviter un plantage.
Exemple de base de données
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
INSERT INTO roles (name) VALUES ('Joueur'), ('Coach'), ('Président');
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
role_id INT NOT NULL,
FOREIGN KEY (role_id) REFERENCES roles(id)
);
—
Classe User
<?php
class User {
private int $id;
private string $name;
private string $email;
private int $role_id;
// --- Hydratation ---
public function hydrate(array $data): void {
foreach ($data as $key => $value) {
$method = 'set' . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
// --- Getters & Setters ---
public function getId(): int { return $this->id; }
public function setId(int $id): void { $this->id = $id; }
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
public function getEmail(): string { return $this->email; }
public function setEmail(string $email): void { $this->email = $email; }
public function getRole_id(): int { return $this->role_id; }
public function setRole_id(int $role_id): void { $this->role_id = $role_id; }
}
?>
On va vérifier que la norme PS12 est appliqué :
Dans votre dossier projet dans le terminal VSC :
On va initialiser Composer qui permet de simplifier l’installation d’outil en PHP
composer init
puis pour installer l’outil :
composer require –dev squizlabs/php_codesniffer
maintenant on a vérifier :
vendor/bin/phpcs –standard=PSR12 User.php
Le terminal doit afficher les erreurs par rapport à la norme PS12
Les corriger !
Pour corriger une partie automatiquement :
vendor/bin/phpcbf –standard=PSR12 User.php
Finir les corrections avec ChatGPT
Faire un commit : Application de la norme PSR12
Classe UserManager
avec gestion d’exceptions
<?php
class UserManager {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// --- Ajouter un utilisateur ---
public function add(User $user): bool {
try {
$sql = "INSERT INTO users (name, email, role_id) VALUES (:name, :email, :role_id)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([
':name' => $user->getName(),
':email' => $user->getEmail(),
':role_id' => $user->getRole_id()
]);
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
// 23000 = violation contrainte (ex: email déjà pris)
echo "L'email existe déjà.";
} else {
echo "Erreur lors de l'ajout : " . $e->getMessage();
}
return false;
}
}
// --- Trouver un utilisateur par son ID ---
public function findById(int $id): ?User {
try {
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
$user = new User();
$user->hydrate($data);
return $user;
}
} catch (PDOException $e) {
echo "Erreur SQL : " . $e->getMessage();
}
return null;
}
// --- Récupérer tous les utilisateurs ---
public function findAll(): array {
$users = [];
try {
$stmt = $this->pdo->query("SELECT * FROM users ORDER BY name");
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
$user = new User();
$user->hydrate($data);
$users[] = $user;
}
} catch (PDOException $e) {
echo "Erreur lors de la récupération : " . $e->getMessage();
}
return $users;
}
}
?>
Remarque :
Le code 23000
que tu vois avec $e->getCode()
est un SQLSTATE (code d’état SQL normalisé).
Signification
-
23000
= Integrity constraint violation (violation d’une contrainte d’intégrité). -
Cela recouvre plusieurs cas typiques :
-
Violation de clé primaire (tu essaies d’insérer une ligne avec un
id
déjà existant). -
Violation de clé unique (UNIQUE) (ex : email déjà présent s’il est UNIQUE).
-
Violation de clé étrangère (FOREIGN KEY) (tu essaies de lier un enregistrement à un id parent inexistant).
-
Parfois des problèmes avec
NOT NULL
si on force une valeur nulle.
-
En pratique, avec MySQL/MariaDB, le cas le plus fréquent est l’insertion d’un doublon sur une colonne marquée UNIQUE.
Si tu veux savoir si c’est uniquement par exemple la clé primaire qui génère l’erreur par exemple, il faut utiliser :
catch (PDOException $e) {
if ($e->getCode() == "23000") {
$info = $e->errorInfo;
// $info[0] = SQLSTATE (23000)
// $info[1] = code d'erreur MySQL spécifique
// $info[2] = message MySQL complet
if ($info[1] == 1062) {
echo "Doublon (clé primaire ou clé unique).";
} elseif ($info[1] == 1452) {
echo "Violation de clé étrangère (FOREIGN KEY).";
} else {
echo "Violation d'une contrainte d'intégrité (autre).";
}
} else {
// Pour toute autre erreur SQL
echo "Erreur SQL : " . htmlspecialchars($e->getMessage());
}
}
Les exceptions PDO sont interceptées pour :
- Fournir un message clair à l’utilisateur.
- Éviter de casser le script.
- Différencier les erreurs (ex : doublon d’email avec code 23000).
—
Utilisation concrète
<?php
try {
$pdo = new PDO("mysql:host=localhost;dbname=club_sport;charset=utf8", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$manager = new UserManager($pdo);
// --- Ajouter un joueur ---
$user = new User();
$user->setName("Jean Dupont");
$user->setEmail("jean.dupont@example.com");
$user->setRole_id(1); // 1 = Joueur
$manager->add($user);
// --- Ajouter un coach ---
$coach = new User();
$coach->setName("Marie Coach");
$coach->setEmail("marie.coach@example.com");
$coach->setRole_id(2); // 2 = Coach
$manager->add($coach);
// --- Afficher tous les membres ---
$users = $manager->findAll();
foreach ($users as $u) {
echo $u->getName() . " (" . $u->getEmail() . ")<br>";
}
} catch (PDOException $e) {
// Si la connexion échoue
echo "Impossible de se connecter à la base : " . $e->getMessage();
}
?>
—
Points pédagogiques pour BTS SIO SLAM
- Un objet ↔ une ligne SQL : l’ORM simplifie la manipulation des données.
- Hydratation : remplit automatiquement un objet à partir d’un tableau PDO.
- Manager : centralise toutes les requêtes SQL.
- try / catch : sécurise le code, empêche les messages d’erreur bruts en production.
TD — Mini-ORM en PHP avec PDO, Exceptions et Git
Niveau : BTS SIO SLAM (2e année)
Objectifs :
Créer une application PHP simple qui gère des équipes d’un club sportif en utilisant :
- PDO pour accéder à MySQL avec
try/catch
pour gérer les erreurs - Un mini-ORM (classes + hydratation d’objets)
- Git pour versionner le code et travailler proprement
Initialiser votre dépôt Git (vous pouvez travailler avec VSC)
- Créez un dossier de projet, ex.
club_sport
- Ouvrez un terminal dans ce dossier et lancez :
git init
git branch -M main
- Créez un dépôt vide sur GitHub (nom ex :
club_sport_orm
). - Ajoutez le dépôt distant :
git remote add origin https://github.com/<votre_user>/club_sport_orm.git
- Premier commit :
git add .
git commit -m "Initialisation du projet"
git push -u origin main
—
Étape 1 — Créer la base de données
Ajoute ce fichier scriptStructures.sql dans ton projetCREATE DATABASE IF NOT EXISTS club_sport CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE club_sport; -- Table des rôles CREATE TABLE IF NOT EXISTS roles ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL ); INSERT INTO roles (name) VALUES ('Joueur'), ('Coach'), ('Président'); -- Table des membres CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(150) NOT NULL UNIQUE, role_id INT NOT NULL, CONSTRAINT fk_users_role FOREIGN KEY (role_id) REFERENCES roles(id) ); -- Table des équipes CREATE TABLE IF NOT EXISTS equipes ( id INT AUTO_INCREMENT PRIMARY KEY, nom VARCHAR(100) NOT NULL, niveau ENUM('Débutant','Confirmé') NOT NULL, coach_id INT NOT NULL, CONSTRAINT fk_equipes_coach FOREIGN KEY (coach_id) REFERENCES users(id) ); -- Table d'association joueurs ↔ équipes CREATE TABLE IF NOT EXISTS joueurs_equipes ( equipe_id INT NOT NULL, joueur_id INT NOT NULL, PRIMARY KEY (equipe_id, joueur_id), CONSTRAINT fk_je_equipe FOREIGN KEY (equipe_id) REFERENCES equipes(id), CONSTRAINT fk_je_joueur FOREIGN KEY (joueur_id) REFERENCES users(id) );
Git :
git add .
git commit -m "Ajout du script SQL pour créer la base de données"
git push
Une fois le fichier .sql fait charge le dans MySQLWorkBench et execute le !
Cela devrait créer la structure de ta BDD. Maintenant active ta base de données en double cliquant dessus !
Elle doit apparaitre en surbrillance !
Étape 2 — Ajouter des données d’exemple
Idem ajoute ce fichier ( scriptDonnees.sql) à ton projet
INSERT INTO users (name, email, role_id) VALUES
('Jean Dupont', 'jean.dupont@club.fr', 1),
('Alice Martin', 'alice.martin@club.fr',1),
('Marie Coach', 'marie.coach@club.fr', 2),
('Paul Coach', 'paul.coach@club.fr', 2),
('Jacques Président','jacques.pres@club.fr',3);
Git :
git add .
git commit -m "Ajout d'un script SQL pour remplir les tables avec des exemples"
git push
Vous devez avoir ceci :
Idem chargé ce fichier depuis MySQLWorkbench et lancer ce script pour ajouter les données dans vos tables.
Étape 3 — Créer les classes PHP (Entités)
Fichier : classes/User.php
<?php
class User {
private int $id;
private string $name;
private string $email;
private int $role_id;
public function hydrate(array $data): void {
foreach ($data as $key => $value) {
$method = 'set' . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
public function getId(): int { return $this->id; }
public function setId(int $id): void { $this->$id = $id; }
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
public function getEmail(): string { return $this->email; }
public function setEmail(string $email): void { $this->email = $email; }
public function getRole_id(): int { return $this->role_id; }
public function setRole_id(int $role_id): void { $this->role_id = $role_id; }
}
On peut vérifier que la norme PSR12 est appliqué !
Dans votre dossier dans le terminal VSC
composer init
On installe l’outil qui vérifie que les normes sont respectées.
composer require –dev squizlabs/php_codesniffer
On peut après l’utiliser :
vendor/bin/phpcs –standard=PSR12 User.php
On corrige les erreurs !
Pour corriger une partie automatiquement :
vendor/bin/phpcbf –standard=PSR12 User.php
Après avoir appliqué la norme PSR12 faire un commit : « Application de la norme PSR12 »
Fichier : classes/Equipe.php
<?php
require_once __DIR__ . '/User.php';
class Equipe {
private int $id;
private string $nom;
private string $niveau;
private int $coach_id;
/** @var User[] */
private array $joueurs = [];
public function hydrate(array $data): void {
foreach ($data as $key => $value) {
$method = 'set' . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
public function getId(): int { return $this->id; }
public function setId(int $id): void { $this->id = $id; }
public function getNom(): string { return $this->nom; }
public function setNom(string $nom): void { $this->nom = $nom; }
public function getNiveau(): string { return $this->niveau; }
public function setNiveau(string $niveau): void { $this->niveau = $niveau; }
public function getCoach_id(): int { return $this->coach_id; }
public function setCoach_id(int $coach_id): void { $this->coach_id = $coach_id; }
public function getJoueurs(): array { return $this->joueurs; }
public function setJoueurs(array $joueurs): void { $this->joueurs = $joueurs; }
}
Git :
git add classes/User.php classes/Equipe.php
git commit -m "Ajout des classes User et Equipe avec hydratation"
git push
—
Étape 4 — Créer le manager EquipeManager
avec try/catch
Fichier : classes/EquipeManager.php
<?php
require_once __DIR__ . '/Equipe.php';
class EquipeManager {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function add(Equipe $equipe): int {
try {
$check = $this->pdo->prepare("SELECT id FROM users WHERE id = :id AND role_id = 2");
$check->execute([':id' => $equipe->getCoach_id()]);
if (!$check->fetch()) {
throw new Exception("Coach inexistant ou non autorisé.");
}
$stmt = $this->pdo->prepare("INSERT INTO equipes (nom, niveau, coach_id) VALUES (:nom, :niveau, :coach_id)");
$stmt->execute([
':nom' => $equipe->getNom(),
':niveau' => $equipe->getNiveau(),
':coach_id' => $equipe->getCoach_id()
]);
return (int)$this->pdo->lastInsertId();
} catch (PDOException $e) {
throw new Exception("Erreur BDD (add): " . $e->getMessage(), 0, $e);
}
}
public function addJoueurToEquipe(int $equipeId, int $joueurId): void {
try {
$check = $this->pdo->prepare("SELECT id FROM users WHERE id = :id AND role_id = 1");
$check->execute([':id' => $joueurId]);
if (!$check->fetch()) {
throw new Exception("Le joueur n'existe pas ou n'est pas de rôle Joueur.");
}
$stmt = $this->pdo->prepare("INSERT INTO joueurs_equipes (equipe_id, joueur_id) VALUES (:e, :j)");
$stmt->execute([':e' => $equipeId, ':j' => $joueurId]);
} catch (PDOException $e) {
throw new Exception("Erreur BDD (addJoueurToEquipe): " . $e->getMessage(), 0, $e);
}
}
public function findByIdWithPlayers(int $id): ?Equipe {
try {
$stmt = $this->pdo->prepare("SELECT * FROM equipes WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) return null;
$e = new Equipe();
$e->hydrate($row);
$stmt2 = $this->pdo->prepare("
SELECT u.* FROM joueurs_equipes je
JOIN users u ON u.id = je.joueur_id
WHERE je.equipe_id = :id
");
$stmt2->execute([':id' => $id]);
$joueurs = [];
while ($urow = $stmt2->fetch(PDO::FETCH_ASSOC)) {
$u = new User();
$u->hydrate($urow);
$joueurs[] = $u;
}
$e->setJoueurs($joueurs);
return $e;
} catch (PDOException $e) {
throw new Exception("Erreur BDD (findByIdWithPlayers): " . $e->getMessage(), 0, $e);
}
}
}
Git :
git add classes/EquipeManager.php
git commit -m "Ajout du manager Equipe avec gestion d'exceptions"
git push
—
Étape 5 — Script de test
Fichier : public/test_equipes.php
<?php
require_once __DIR__ . '/../classes/User.php';
require_once __DIR__ . '/../classes/Equipe.php';
require_once __DIR__ . '/../classes/EquipeManager.php';
try {
$pdo = new PDO("mysql:host=localhost;dbname=club_sport;charset=utf8mb4", "root", "");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$manager = new EquipeManager($pdo);
// Créer une équipe
$e = new Equipe();
$e->setNom("Les Lions");
$e->setNiveau("Confirmé");
$e->setCoach_id(3); // Marie Coach
$idEquipe = $manager->add($e);
echo "<p>Equipe créée : ID ".$idEquipe."</p>";
// Ajouter des joueurs
$manager->addJoueurToEquipe($idEquipe, 1); // Jean Dupont
$manager->addJoueurToEquipe($idEquipe, 2); // Alice Martin
// Afficher l'équipe avec ses joueurs
$loaded = $manager->findByIdWithPlayers($idEquipe);
echo "<h3>".$loaded->getNom()." (".$loaded->getNiveau().")</h3>";
echo "<ul>";
foreach($loaded->getJoueurs() as $j){
echo "<li>".$j->getName()." - ".$j->getEmail()."</li>";
}
echo "</ul>";
} catch(Exception $e) {
echo "<p style='color:red'>Erreur : ".htmlspecialchars($e->getMessage())."</p>";
}
Git :
git add public/test_equipes.php
git commit -m "Ajout du script de test pour créer une équipe et associer des joueurs"
git push
—
Étape 6 — Cas à tester
- ✅ Créer une équipe avec un coach existant → doit s’insérer
- ❌ Créer avec un coach inexistant → message d’erreur
- ❌ Ajouter un joueur inexistant → message d’erreur
- ❌ Ajouter un utilisateur qui n’est pas un joueur → message d’erreur
—
Étape 7 — Bonnes pratiques Git
- Commitez souvent : 1 commit = 1 fonctionnalité claire.
- Poussez régulièrement sur GitHub pour sauvegarder.
- Utilisez des branches pour les nouvelles fonctionnalités :
git checkout -b feature/affichage_equipes
Étape 8 — Commentaires & Docblocks
Pourquoi documenter ?
- Clarifie l’intention et les préconditions d’une méthode (contrats d’usage).
- Aide les IDE (VS Code / PhpStorm) pour l’auto-complétion et la navigation.
- Permet de générer une doc HTML via des outils (phpDocumentor, Doctum, etc.).
Rappels de syntaxe (PHPDoc)
Un docblock se place juste au-dessus d’une classe/méthode/attribut :
/**
* Brève description.
*
* Description détaillée (optionnelle).
*
* @tag1 ...
* @tag2 ...
*/
Tags les plus utilisés :
@param type $nom
: décrit un paramètre@return type
: type de retour@throws \ExceptionType
: exception potentielle@var type
: type d’un attribut@author
,@version
(optionnels)
Exemple appliqué à votre TD (classe User
)
<?php
declare(strict_types=1);
/**
* Représente un utilisateur du club (entité métier).
*/
class User
{
/**
* Identifiant unique.
* @var int
*/
private int $id;
/**
* Nom complet.
* @var string
*/
private string $name;
/**
* Adresse e-mail.
* @var string
*/
private string $email;
/**
* Identifiant du rôle (clé étrangère vers la table roles).
* @var int
*/
private int $roleId;
/**
* Hydrate l'objet à partir d'un tableau associatif.
*
* @param array $data Tableau [cle => valeur] (ex. issu d'un fetch PDO)
* @return void
*/
public function hydrate(array $data): void
{
foreach ($data as $key => $value) {
$method = 'set' . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
/** @return int */
public function getId(): int { return $this->id; }
/** @param int $id */
public function setId(int $id): void { $this->id = $id; }
/** @return string */
public function getName(): string { return $this->name; }
/** @param string $name */
public function setName(string $name): void { $this->name = $name; }
/** @return string */
public function getEmail(): string { return $this->email; }
/** @param string $email */
public function setEmail(string $email): void { $this->email = $email; }
/** @return int */
public function getRoleId(): int { return $this->roleId; }
/** @param int $roleId */
public function setRoleId(int $roleId): void { $this->roleId = $roleId; }
}
Exemple appliqué au UserManager
(PDO + exceptions)
<?php
declare(strict_types=1);
/**
* Service d'accès aux utilisateurs (CRUD).
*/
class UserManager
{
private \PDO $pdo;
/**
* @param \PDO $pdo Connexion PDO (injectée)
*/
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Insère un nouvel utilisateur.
*
* @param User $user Entité à persister
* @return bool True si insertion OK
* @throws \PDOException En cas d'erreur SQL
*/
public function add(User $user): bool
{
try {
$sql = "INSERT INTO users (name, email, role_id) VALUES (:name, :email, :role_id)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([
':name' => $user->getName(),
':email' => $user->getEmail(),
':role_id' => $user->getRoleId(),
]);
} catch (\PDOException $e) {
// 23000 = violation d'intégrité (clé unique / clé étrangère)
if ($e->getCode() === '23000') {
$info = $e->errorInfo;
if (($info[1] ?? null) === 1062) {
// Doublon (clé unique)
throw new \PDOException("E-mail déjà utilisé (doublon).", 1062, $e);
}
if (($info[1] ?? null) === 1452) {
// Clé étrangère non respectée
throw new \PDOException("Rôle inexistant (violation de clé étrangère).", 1452, $e);
}
}
throw $e; // remonter l'erreur si non gérable ici
}
}
/**
* Récupère un utilisateur par son identifiant.
*
* @param int $id Identifiant
* @return User|null L'entité ou null si non trouvée
*/
public function findById(int $id): ?User
{
$stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$u = new User();
$u->hydrate($row);
return $u;
}
}
Travail à faire
- Ajouter des docblocks complets à vos classes
User
,Role
,Equipe
,UserManager
, etc. (toutes les méthodes publiques au minimum). - Documenter précisément :
- le rôle de la classe/méthode,
- les paramètres (
@param
type + signification), - le type de retour (
@return
), - les exceptions possibles (
@throws
), - les attributs non triviaux (
@var
).
- Dans les blocs
catch
, expliquer en 1–2 lignes ce que vous traitez (ex. code SQLState 23000, cas 1062/1452, stratégie choisie). - Vérifier la cohérence types PHP ↔ types PHPDoc (PSR-12 recommandé).
Checklist d’auto-évaluation
- Chaque fichier PHP commence par
declare(strict_types=1);
et unnamespace
cohérent. - Toutes les méthodes publiques ont un docblock
@param
/@return
exact. - Les exceptions métiers ou PDO sont documentées avec
@throws
. - Les parties « non évidentes » du code ont un court commentaire au-dessus (1–2 lignes max). Dire pourquoi vous avez fait ça ! Pas ce que vous faites !
- Le projet passe au vert avec phpcs (PSR-12) et les types PHPDoc correspondent au code.
Outils utiles
- Vérification PSR-12 :
composer require --dev squizlabs/php_codesniffer
puisvendor/bin/phpcs --standard=PSR12 src/
- Correction auto :
vendor/bin/phpcbf --standard=PSR12 src/
- Génération de doc : phpDocumentor / Doctum (facultatif pour ce TD).
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.