Tutoriel MVC PHP (PDO / MySQL)
Tutoriel MVC PHP avec PDO / MySQL – CRUD Complet
-
Objectif du TP
- À la fin de ce TP, l’apprenant sera capable de :
- Mettre en place une CRUD complète (Create, Read, Update, Delete) sans framework.
- Utiliser PDO (PHP Data Objects) pour interagir de manière sécurisée avec MySQL.
- Structurer une application PHP proprement selon le pattern MVC (Modèle – Vue – Contrôleur).
- Créer un modèle
Userrelié à une table MySQL. - Utiliser un contrôleur pour gérer les actions CRUD.
- Afficher les données dans des vues propres et sécurisées.
- Comprendre le cheminement complet d’une requête dans une architecture MVC.
-
Rappel – Qu’est-ce que le MVC ?
Le pattern MVC sépare l’application en trois couches distinctes :- Modèle (Model) : accès aux données, SQL, logique métier.
- Vue (View) : affichage HTML, aucune logique métier.
- Contrôleur (Controller) : réception des requêtes, appel du modèle, choix de la vue.
-
Création de la base de données
- Nous allons créer une base de données
mvc_appcontenant une tableuserspour notre CRUD. - Exécutez ce script SQL dans phpMyAdmin ou votre terminal MySQL :
- Remarque : L’attribut
UNIQUEsuremailempêche les doublons au niveau de la base de données. -
Architecture complète du projet MVC (CRUD Users)
- Voici l’arborescence complète du projet à créer manuellement :
- 👉 Tous ces fichiers doivent être créés manuellement.
- Principe du Front Controller : toutes les requêtes HTTP passent par
public/index.php, qui délègue ensuite au bon contrôleur via le routeur. -
Fichiers
-
public/index.php – Point d’entrée unique (Front Controller)
- Chemin :
public/index.php - Rôle : démarre l’application MVC – c’est le seul fichier appelé directement par le navigateur.
- Toutes les requêtes passent obligatoirement par ce fichier grâce au .htaccess.
-
.htaccess – Réécriture d’URL (à la racine du projet)
- Chemin :
.htaccess(à la racine du projet) - Sert à :
- Masquer les fichiers internes (modèles, config, etc.) du navigateur.
- Forcer toutes les URLs à passer par le Front Controller MVC.
- Permettre des URLs propres comme
/usersou/users/edit/3. - ⚠️ Prérequis : le module
mod_rewritedoit être activé dans Apache. -
config/database.php – Configuration de la base de données
- Chemin :
config/database.php - Bonne pratique : aucun SQL ici, uniquement la configuration. En production, ne commitez jamais ce fichier dans Git (ajoutez-le à
.gitignore). -
core/Model.php – Classe de base PDO (parent de tous les modèles)
- Chemin :
core/Model.php - Tous les modèles héritent de cette classe via
class User extends Model. - Avantage : la connexion est instanciée une seule fois par modèle, centralisée et facile à maintenir.
-
core/Router.php – Routeur URL
- Chemin :
core/Router.php -
routes/web.php – Déclaration des routes
- Chemin :
routes/web.php -
app/models/User.php – Modèle User (toutes les opérations SQL)
- Chemin :
app/models/User.php - ✅ Règle d’or : SQL uniquement ici – jamais de HTML dans un modèle.
- Sécurité : toutes les requêtes avec données utilisateur utilisent des requêtes préparées (
prepare+execute) pour éviter les injections SQL. -
app/controllers/UserController.php – Contrôleur User
- Chemin :
app/controllers/UserController.php - ✅ Bonne pratique : toujours ajouter
exit;après unheader("Location: ...")pour éviter que le script continue à s’exécuter. -
Vue : Liste des utilisateurs
- Chemin :
app/views/users/index.php - La variable
$usersest transmise par le contrôleur. La vue ne fait qu’afficher les données. - Sécurité :
htmlspecialchars()convertit les caractères spéciaux HTML (<,>, etc.) en entités HTML pour éviter les attaques XSS. - Sécurité :
(int)$user['id']force le transtypage en entier pour éviter toute injection dans l’URL. -
Vue : Création d’un utilisateur
- Chemin :
app/views/users/create.php - Note : les attributs
requiredettype="email"offrent une première validation côté navigateur. La validation côté serveur (dans le contrôleur) reste indispensable. -
Vue : Modification d’un utilisateur
- Chemin :
app/views/users/edit.php - La variable
$datacontient les informations actuelles de l’utilisateur, transmises par le contrôleur. -
Schéma du flux MVC – Comment fonctionne une requête ?
- Voici le cheminement complet d’une requête dans notre application MVC :
-
À retenir
- ✅ Le Modèle contient tout le SQL – rien d’autre.
- ✅ Le Contrôleur gère la logique : reçoit la requête, appelle le modèle, choisit la vue.
- ✅ La Vue affiche les données – aucune logique métier, aucun SQL.
- ✅ Toujours utiliser des requêtes préparées (PDO
prepare+execute) pour sécuriser les requêtes SQL. - ✅ Toujours utiliser
htmlspecialchars()dans les vues pour protéger contre le XSS. - ✅ Toujours ajouter
exit;aprèsheader("Location: ..."). - ❌ Pas de SQL dans les vues.
- ❌ Pas de HTML dans les modèles.
- ❌ Pas de logique métier complexe dans les vues.
- ✅ MVC = structure claire, maintenable et professionnelle.
-- Création de la base de données
CREATE DATABASE mvc_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Sélection de la base
USE mvc_app;
-- Création de la table users
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, -- Identifiant unique auto-incrémenté
name VARCHAR(100) NOT NULL, -- Nom de l'utilisateur (obligatoire)
email VARCHAR(150) NOT NULL UNIQUE -- Email unique (obligatoire)
);
-- Insertion de données de test
INSERT INTO users (name, email) VALUES
('Alice Martin', 'alice@example.com'),
('Bob Dupont', 'bob@example.com');
mvc-users/
│
├── app/ ← Code applicatif (MVC)
│ ├── controllers/
│ │ └── UserController.php ← Contrôleur : gère les actions CRUD
│ │
│ ├── models/
│ │ └── User.php ← Modèle : requêtes SQL sur la table users
│ │
│ └── views/
│ └── users/
│ ├── index.php ← Vue : liste des utilisateurs
│ ├── create.php ← Vue : formulaire d'ajout
│ └── edit.php ← Vue : formulaire de modification
│
├── core/ ← Classes de base du framework maison
│ ├── Model.php ← Connexion PDO partagée
│ ├── Controller.php ← Classe parente des contrôleurs
│ └── Router.php ← Routeur URL → contrôleur/action
│
├── config/
│ └── database.php ← Paramètres de connexion BDD
│
├── routes/
│ └── web.php ← Déclaration des routes de l'application
│
├── public/
│ └── index.php ← Point d'entrée unique (Front Controller)
│
├── .htaccess ← Réécriture d'URL vers public/index.php
└── README.md ← Documentation du projet
<?php
// Chargement du routeur central
require_once '../core/Router.php';
// Instanciation du routeur
$router = new Router();
// Chargement des routes déclarées dans routes/web.php
require_once '../routes/web.php';
// Démarrage du routeur : analyse l'URL et dispatche vers le bon contrôleur
$router->run();
RewriteEngine On
# Si le fichier ou dossier demandé n'existe pas physiquement sur le serveur
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirige toutes les requêtes vers public/index.php
RewriteRule ^(.*)$ public/index.php [QSA,L]
<?php
// Retourne un tableau de configuration pour la connexion PDO
// Ce fichier est chargé par core/Model.php
return [
'host' => 'localhost', // Hôte MySQL (souvent localhost en dev)
'dbname' => 'mvc_app', // Nom de la base de données créée précédemment
'user' => 'root', // Utilisateur MySQL
'password' => '' // Mot de passe (vide par défaut avec XAMPP/WAMP)
];
<?php
/**
* Classe Model – Classe parente de tous les modèles de l'application.
* Elle centralise la connexion PDO pour éviter de la répéter dans chaque modèle.
*/
class Model
{
// Propriété partagée : instance PDO accessible dans toutes les sous-classes
protected $db;
public function __construct()
{
// Chargement de la configuration depuis config/database.php
$config = require '../config/database.php';
// Création de la connexion PDO avec le DSN MySQL
$this->db = new PDO(
"mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4",
$config['user'],
$config['password']
);
// Mode d'erreur : lance une exception en cas d'erreur SQL (facilite le débogage)
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Retourne les résultats sous forme de tableaux associatifs par défaut
$this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
}
<?php
/**
* Classe Router – Analyse l'URL et dispatche vers le bon contrôleur et la bonne action.
*/
class Router
{
// Tableau des routes enregistrées : ['METHOD /chemin' => callable]
private $routes = [];
/**
* Enregistre une route GET.
* @param string $path L'URL à matcher (ex: '/users')
* @param callable $callback La fonction ou méthode à exécuter
*/
public function get($path, $callback)
{
$this->routes['GET ' . $path] = $callback;
}
/**
* Enregistre une route POST.
*/
public function post($path, $callback)
{
$this->routes['POST ' . $path] = $callback;
}
/**
* Démarre le routeur : compare l'URL courante aux routes enregistrées.
*/
public function run()
{
// Récupère la méthode HTTP (GET ou POST)
$method = $_SERVER['REQUEST_METHOD'];
// Récupère le chemin URL sans les paramètres de requête
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route => $callback) {
// Convertit les segments dynamiques comme {id} en regex
[$routeMethod, $routePath] = explode(' ', $route, 2);
$pattern = preg_replace('/\{[a-z]+\}/', '([^/]+)', $routePath);
$pattern = '#^' . $pattern . '$#';
if ($routeMethod === $method && preg_match($pattern, $uri, $matches)) {
// Supprime le match complet, garde uniquement les paramètres
array_shift($matches);
// Appelle le callback avec les paramètres extraits de l'URL
call_user_func_array($callback, $matches);
return;
}
}
// Aucune route correspondante → erreur 404
http_response_code(404);
echo "<h1>404 – Page non trouvée</h1>";
}
}
<?php
// Chargement du contrôleur utilisateur
require_once '../app/controllers/UserController.php';
$controller = new UserController();
// Affiche la liste des utilisateurs
$router->get('/users', function() use ($controller) {
$controller->index();
});
// Affiche le formulaire de création
$router->get('/users/create', function() use ($controller) {
$controller->create();
});
// Traite le formulaire de création (POST)
$router->post('/users/store', function() use ($controller) {
$controller->store();
});
// Affiche le formulaire de modification pour l'utilisateur {id}
$router->get('/users/edit/{id}', function($id) use ($controller) {
$controller->edit($id);
});
// Traite le formulaire de modification (POST)
$router->post('/users/update/{id}', function($id) use ($controller) {
$controller->update($id);
});
// Supprime l'utilisateur {id}
$router->get('/users/delete/{id}', function($id) use ($controller) {
$controller->delete($id);
});
<?php
require_once '../core/Model.php';
/**
* Classe User – Modèle représentant la table `users`.
* Contient UNIQUEMENT les opérations SQL (jamais de HTML ici).
*/
class User extends Model
{
/**
* Récupère tous les utilisateurs de la table.
* @return array Tableau de tous les utilisateurs
*/
public function getAll()
{
// query() suffit pour une requête sans paramètre dynamique
return $this->db
->query("SELECT * FROM users ORDER BY id DESC")
->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Récupère un utilisateur par son identifiant.
* @param int $id L'identifiant de l'utilisateur
* @return array|false Les données de l'utilisateur ou false si non trouvé
*/
public function getById($id)
{
// prepare() + execute() = requête préparée → protège contre les injections SQL
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
/**
* Insère un nouvel utilisateur.
* @param string $name Le nom de l'utilisateur
* @param string $email L'adresse email
* @return bool true en cas de succès
*/
public function create($name, $email)
{
$stmt = $this->db->prepare(
"INSERT INTO users (name, email) VALUES (?, ?)"
);
return $stmt->execute([$name, $email]);
}
/**
* Met à jour les données d'un utilisateur existant.
* @param int $id L'identifiant de l'utilisateur à modifier
* @param string $name Le nouveau nom
* @param string $email Le nouvel email
* @return bool true en cas de succès
*/
public function update($id, $name, $email)
{
$stmt = $this->db->prepare(
"UPDATE users SET name = ?, email = ? WHERE id = ?"
);
// Attention à l'ordre des paramètres : name, email, puis id
return $stmt->execute([$name, $email, $id]);
}
/**
* Supprime un utilisateur par son identifiant.
* @param int $id L'identifiant de l'utilisateur à supprimer
* @return bool true en cas de succès
*/
public function delete($id)
{
$stmt = $this->db->prepare("DELETE FROM users WHERE id = ?");
return $stmt->execute([$id]);
}
}
<?php
require_once '../app/models/User.php';
/**
* Classe UserController – Contrôleur gérant toutes les actions CRUD sur les utilisateurs.
* Le contrôleur fait le lien entre le modèle (données) et la vue (affichage).
* Il ne contient ni SQL, ni HTML brut.
*/
class UserController
{
/**
* Action index – Affiche la liste de tous les utilisateurs.
* Correspond à la route : GET /users
*/
public function index()
{
$model = new User();
$users = $model->getAll(); // Récupère les données via le modèle
require '../app/views/users/index.php'; // Délègue l'affichage à la vue
}
/**
* Action create – Affiche le formulaire de création d'un utilisateur.
* Correspond à la route : GET /users/create
*/
public function create()
{
require '../app/views/users/create.php';
}
/**
* Action store – Traite le formulaire de création (méthode POST).
* Correspond à la route : POST /users/store
*/
public function store()
{
// Récupération et nettoyage des données du formulaire
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
// Validation basique avant insertion
if (!empty($name) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$model = new User();
$model->create($name, $email);
}
// Redirection vers la liste après enregistrement
header("Location: /users");
exit; // Important : stoppe l'exécution après un header redirect
}
/**
* Action edit – Affiche le formulaire de modification d'un utilisateur.
* Correspond à la route : GET /users/edit/{id}
* @param int $id L'identifiant de l'utilisateur à modifier
*/
public function edit($id)
{
$model = new User();
$data = $model->getById($id); // Récupère les données actuelles de l'utilisateur
if (!$data) {
// Si l'utilisateur n'existe pas, redirection vers la liste
header("Location: /users");
exit;
}
require '../app/views/users/edit.php'; // Passe $data à la vue
}
/**
* Action update – Traite le formulaire de modification (méthode POST).
* Correspond à la route : POST /users/update/{id}
* @param int $id L'identifiant de l'utilisateur à mettre à jour
*/
public function update($id)
{
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
if (!empty($name) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
$model = new User();
$model->update($id, $name, $email);
}
header("Location: /users");
exit;
}
/**
* Action delete – Supprime un utilisateur.
* Correspond à la route : GET /users/delete/{id}
* @param int $id L'identifiant de l'utilisateur à supprimer
*/
public function delete($id)
{
$model = new User();
$model->delete($id);
header("Location: /users");
exit;
}
}
<!-- Lien vers le formulaire de création -->
<a href="/users/create">➕ Ajouter un utilisateur</a>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($users)): ?>
<!-- Cas où la table est vide -->
<tr>
<td colspan="4">Aucun utilisateur trouvé.</td>
</tr>
<?php else: ?>
<?php foreach ($users as $user): ?>
<tr>
<td><?= $user['id'] ?></td>
<!-- htmlspecialchars() protège contre les attaques XSS -->
<td><?= htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($user['email'], ENT_QUOTES, 'UTF-8') ?></td>
<td>
<a href="/users/edit/<?= (int)$user['id'] ?>">✏️ Modifier</a>
<!-- Idéalement, la suppression devrait passer par un formulaire POST -->
<a href="/users/delete/<?= (int)$user['id'] ?>"
onclick="return confirm('Confirmer la suppression ?')">
🗑️ Supprimer
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<h2>Ajouter un utilisateur</h2>
<!-- action pointe vers la route POST /users/store -->
<form method="post" action="/users/store">
<label for="name">Nom :</label><br>
<input type="text"
id="name"
name="name"
placeholder="Ex : Alice Martin"
required
maxlength="100">
<br><br>
<label for="email">Email :</label><br>
<input type="email"
id="email"
name="email"
placeholder="Ex : alice@example.com"
required
maxlength="150">
<br><br>
<button type="submit">💾 Enregistrer</button>
<a href="/users">Annuler</a>
</form>
<h2>Modifier l'utilisateur</h2>
<!-- L'id est intégré dans l'URL de l'action -->
<form method="post" action="/users/update/<?= (int)$data['id'] ?>">
<label for="name">Nom :</label><br>
<!-- value pré-remplit le champ avec la valeur actuelle –
htmlspecialchars protège contre le XSS -->
<input type="text"
id="name"
name="name"
value="<?= htmlspecialchars($data['name'], ENT_QUOTES, 'UTF-8') ?>"
required
maxlength="100">
<br><br>
<label for="email">Email :</label><br>
<input type="email"
id="email"
name="email"
value="<?= htmlspecialchars($data['email'], ENT_QUOTES, 'UTF-8') ?>"
required
maxlength="150">
<br><br>
<button type="submit">✏️ Mettre à jour</button>
<a href="/users">Annuler</a>
</form>
Navigateur
│
│ GET /users
▼
.htaccess
│ Redirige tout vers public/index.php
▼
public/index.php (Front Controller)
│ Instancie le Router et charge les routes
▼
core/Router.php
│ Analyse l'URL, trouve la route correspondante
▼
app/controllers/UserController.php → action index()
│ Appelle le modèle pour récupérer les données
▼
app/models/User.php → getAll()
│ Exécute la requête SQL via PDO
│ Retourne un tableau de résultats
▼
UserController::index()
│ Passe les données ($users) à la vue
▼
app/views/users/index.php
│ Génère le HTML avec les données
▼
Navigateur (affichage de la page)
