Module 4Réaliser un site web avec l'architecture MVC
Chapitre 9 sur 11
Développer des sites dynamiques en MVC
Organiser une application en modèles, vues et contrôleurs avec un routeur.
- ≈ 125 min
- 5 leçons
- 2 exercices
- TP
Module 4Réaliser un site web avec l'architecture MVC
Chapitre 9 sur 11
Organiser une application en modèles, vues et contrôleurs avec un routeur.
Objectifs
Avec ce chapitre, tu sauras…
Leçon 1
NAVIGATEUR ── 1. requête : index.php?action=liste ──► ROUTEUR (index.php)
│ 2. appelle la bonne action
▼
CONTRÔLEUR
3. demande les │ ▲ 4. renvoie les
données ▼ │ données
MODÈLE ◄──► base MySQL (SQL)
│
5. le contrôleur transmet les données à la VUE
▼
NAVIGATEUR ◄──────────── 6. page HTML ─────────────── VUE (gabarit HTML)| Avantages | Inconvénients |
|---|---|
| code mieux organisé : chaque fichier a un rôle | plus complexe pour un tout petit site |
| réutilisation : un modèle sert à plusieurs pages, une vue à plusieurs données | plus de fichiers à gérer |
| maintenance et évolution plus simples : changer le design ne touche pas au SQL | demande un temps de conception |
| travail en équipe : l'intégrateur travaille sur les vues, le développeur sur les modèles | |
| tests plus faciles (le modèle se teste seul) |
Leçon 2
Leçon 3
Leçon 4
Leçon 5
| Framework | Particularité |
|---|---|
| Laravel | le plus populaire ; syntaxe élégante, ORM Eloquent, gabarits Blade, très complet |
| Symfony | composants réutilisables (Laravel en utilise plusieurs), très utilisé en entreprise en France et au Maroc |
| CodeIgniter | léger, simple à apprendre |
| CakePHP, Yii, Laminas (ex-Zend), Phalcon | autres frameworks complets |
| Slim, Lumen | micro-frameworks, pour les petites applications et les API |
1Dans MVC, la partie qui affiche la page HTML est…
2La partie qui exécute les requêtes SQL est…
3Le routeur est souvent dans index.php à la racine parce que…
4Le bon enchaînement est…
5Une vue peut contenir…
6Un inconvénient du MVC est…
L'essentiel du chapitre
index.php : point d'entrée unique, ?action=… → fonction ou méthode du contrôleur ; gabarit commun avec ob_start() ; URL propres avec mod_rewrite.…Modele et …Controleur, autoload, injection de dépendances.En entreprise
Pourquoi apprendre le MVC « natif » d'abord ?
On part d'une table comments (id, auteur, commentaire, date_commentaire) dans la base test.
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
auteur VARCHAR(50) NOT NULL,
commentaire TEXT NOT NULL,
date_commentaire DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);Le modèle : modele.php
Dans une architecture MVC, on commence en général par écrire le modèle.
<?php
function bdConnect(): PDO
{
static $bd = null;
$bd ??= new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'app_test', 'Test#2026!', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
return $bd;
}
function getLastFiveComments(): array
{
$req = bdConnect()->query(
"SELECT id, auteur, commentaire,
DATE_FORMAT(date_commentaire, '%d/%m/%Y à %Hh%i') AS date_fr
FROM comments
ORDER BY date_commentaire DESC
LIMIT 5"
);
return $req->fetchAll();
}
function getCommentById(int $id): array|false
{
$req = bdConnect()->prepare(
"SELECT id, auteur, commentaire,
DATE_FORMAT(date_commentaire, '%d/%m/%Y à %Hh%i') AS date_fr
FROM comments WHERE id = ?"
);
$req->execute([$id]);
return $req->fetch();
}
function addComment(string $auteur, string $texte): void
{
$req = bdConnect()->prepare('INSERT INTO comments (auteur, commentaire) VALUES (?, ?)');
$req->execute([$auteur, $texte]);
}L'exemple du résumé écrit ORDER BY date_ commentaire avec une espace dans le nom de colonne (erreur SQL), et les guillemets typographiques ‘ ’ autour de la requête provoquent une erreur de syntaxe PHP. Par ailleurs, une requête qui cherche un commentaire par son identifiant renvoie une seule ligne : fetch() suffit, le tri est inutile.
La vue : vue_liste.php
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Mon site web dynamique</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<h1>Bonjour</h1>
<p>Derniers commentaires du site :</p>
<?php foreach ($comments as $c): ?>
<div class="news">
<p>
<strong><?= htmlspecialchars($c['auteur']) ?></strong> le <?= $c['date_fr'] ?><br>
<?= nl2br(htmlspecialchars($c['commentaire'])) ?>
</p>
</div>
<?php endforeach; ?>
</body>
</html>Une vue contient du HTML et seulement de petits éléments PHP d'affichage : <?= ?>, foreach, if avec la syntaxe alternative, htmlspecialchars. Elle ne contient aucune requête SQL, aucun accès à $_POST, aucun traitement.
Le contrôleur : index.php
<?php
require 'modele.php';
$comments = getLastFiveComments(); // 1. demander les données au modèle
require 'vue_liste.php'; // 2. afficher la vue, qui utilise $commentsDès qu'il y a plusieurs pages, index.php devient un routeur (ou contrôleur frontal) : c'est le point d'entrée unique de l'application. Il lit l'action demandée et appelle la bonne fonction du contrôleur.
mvc/
├── index.php ← routeur : point d'entrée unique
├── controleur/
│ └── controleur.php ← une fonction par action
├── modele/
│ └── modele.php ← accès aux données
└── vue/
├── gabarit.php ← le squelette HTML commun
├── liste.php
├── commentaire.php
└── erreur.php<?php
require 'controleur/controleur.php';
try {
$action = $_GET['action'] ?? 'liste';
match ($action) {
'liste' => listeCommentaires(),
'voir' => voirCommentaire((int) ($_GET['id'] ?? 0)),
'ajouter' => ajouterCommentaire($_POST),
default => throw new Exception("Action inconnue : $action"),
};
} catch (Exception $e) {
afficherErreur($e->getMessage());
}<?php
require __DIR__ . '/../modele/modele.php';
function listeCommentaires(): void
{
$comments = getLastFiveComments();
afficher('liste', ['comments' => $comments, 'titre' => 'Derniers commentaires']);
}
function voirCommentaire(int $id): void
{
$comment = getCommentById($id);
if (!$comment) {
throw new Exception('Commentaire introuvable.');
}
afficher('commentaire', ['comment' => $comment, 'titre' => 'Commentaire']);
}
function ajouterCommentaire(array $post): void
{
$auteur = trim($post['auteur'] ?? '');
$texte = trim($post['commentaire'] ?? '');
if ($auteur === '' || $texte === '') {
throw new Exception('Tous les champs sont obligatoires.');
}
addComment($auteur, $texte);
header('Location: index.php?action=liste');
exit;
}
function afficherErreur(string $message): void
{
http_response_code(400);
afficher('erreur', ['message' => $message, 'titre' => 'Erreur']);
}
function afficher(string $vue, array $donnees): void
{
extract($donnees); // ['titre' => …] crée la variable $titre
ob_start(); // capturer l'affichage de la vue…
require __DIR__ . "/../vue/$vue.php";
$contenu = ob_get_clean(); // …dans la variable $contenu
require __DIR__ . '/../vue/gabarit.php'; // puis l'insérer dans le gabarit commun
}<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title><?= htmlspecialchars($titre) ?></title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<header><a href="index.php">Mon site</a></header>
<main><?= $contenu ?></main>
<footer>M107 · OFPPT</footer>
</body>
</html><h1><?= htmlspecialchars($titre) ?></h1>
<?php foreach ($comments as $c): ?>
<article>
<a href="index.php?action=voir&id=<?= $c['id'] ?>"><?= htmlspecialchars($c['auteur']) ?></a>
<small><?= $c['date_fr'] ?></small>
<p><?= nl2br(htmlspecialchars($c['commentaire'])) ?></p>
</article>
<?php endforeach; ?>
<form method="post" action="index.php?action=ajouter">
<input name="auteur" placeholder="Ton nom" required>
<textarea name="commentaire" placeholder="Ton commentaire" required></textarea>
<button>Publier</button>
</form>Des URL propres avec mod_rewrite
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^commentaire/([0-9]+)$ index.php?action=voir&id=$1 [L,QSA]
RewriteRule ^commentaires$ index.php?action=liste [L,QSA]Les deux RewriteCond laissent passer les fichiers et dossiers qui existent vraiment (style.css, images). Le module mod_rewrite doit être activé et AllowOverride All autorisé pour le dossier (chapitre 3).
Dans les vrais projets, modèles et contrôleurs sont des classes (chapitre 6), chargées par l'autoload.
<?php
namespace App\Modele;
use PDO;
class CommentaireModele
{
public function __construct(private PDO $pdo) {}
public function derniers(int $n = 5): array
{
$req = $this->pdo->prepare('SELECT * FROM comments ORDER BY date_commentaire DESC LIMIT :n');
$req->bindValue('n', $n, PDO::PARAM_INT);
$req->execute();
return $req->fetchAll();
}
}<?php
namespace App\Controleur;
use App\Modele\CommentaireModele;
class CommentaireControleur
{
public function __construct(private CommentaireModele $modele) {}
public function liste(): void
{
$comments = $this->modele->derniers();
require __DIR__ . '/../../vue/liste.php';
}
}Le contrôleur reçoit son modèle dans son constructeur au lieu de le créer lui-même, et le modèle reçoit sa connexion PDO. On parle d'injection de dépendances : chaque classe est plus simple à tester (on peut lui donner une base de test) et à réutiliser. Les frameworks le font automatiquement.
Réorganise ton CRUD des chapitres 7 et 8 en MVC : un routeur index.php (actions liste, voir, ajouter, modifier, supprimer, connexion, deconnexion), une classe StagiaireModele (méthodes tous(), parId(), ajouter(), modifier(), supprimer()), une classe StagiaireControleur, un gabarit commun et une vue par page. Garde la sécurité : requêtes préparées, htmlspecialchars, jetons CSRF, exigerConnexion(). Bonus : des URL propres avec .htaccess (/stagiaires, /stagiaires/12).
Ce fichier mélange tout. Découpe-le en un modèle (getProduits()), une vue (vue_produits.php) et un contrôleur (index.php).
<?php
$pdo = new PDO('mysql:host=localhost;dbname=boutique;charset=utf8mb4', 'app', 'Mdp#2026');
$produits = $pdo->query('SELECT nom, prix FROM produits ORDER BY prix')->fetchAll(PDO::FETCH_ASSOC);
?>
<h1>Nos produits</h1>
<ul>
<?php foreach ($produits as $p): ?>
<li><?= htmlspecialchars($p['nom']) ?> : <?= number_format($p['prix'], 2, ',', ' ') ?> DH</li>
<?php endforeach; ?>
</ul>Un framework ajoute des conventions et beaucoup de fonctionnalités (routeur, ORM, validation, sécurité). Quand tu as écrit toi-même un routeur, des modèles et des vues, tu comprends ce que fait le framework à ta place, et tu l'apprends beaucoup plus vite.