Le restaurant
Le serveur (contrôleur) prend la commande, la transmet à la cuisine (modèle) qui prépare le plat avec les ingrédients du stock (la base), puis il apporte l'assiette dressée (vue). Le client ne parle jamais directement au cuisinier.
Séparer un site PHP en modèles (les données), vues (l'affichage) et contrôleurs (la logique de chaque page), le rôle du contrôleur frontal index.php et du routage par ?action=, la circulation d'une requête dans les trois couches, et l'arborescence d'un projet MVC.
Au fil des leçons, les pages de Souk Atlas mélangent tout : connexion, requêtes SQL, tests, HTML. À dix pages, c'est encore lisible ; à cinquante, plus personne ne s'y retrouve. Le modèle MVC range chaque morceau à sa place. C'est aussi l'organisation imposée par les frameworks (Laravel, Symfony) que vous rencontrerez ensuite.
Le navigateur demande index.php?action=detail&id=9. Toutes les requêtes arrivent au même fichier, le contrôleur frontal.
| Couche | Ce qu’elle contient | Ce qu’elle ne contient jamais |
|---|---|---|
| Modèle | classes avec PDO, SELECT, INSERT, calculs sur les données | echo, HTML, $_GET |
| Vue | HTML, <?= ?>, foreach d’affichage, htmlspecialchars | SQL, redirections |
| Contrôleur | lecture de $_GET/$_POST, vérification de session, choix de la vue, header('Location: ...') | SQL, gros blocs HTML |
Une arborescence courante :
souk-atlas/
├── index.php ← contrôleur frontal (seul fichier appelé par le navigateur)
├── config/db.php ← connexion PDO
├── controllers/ProduitController.php
├── models/ProduitModel.php
└── views/
├── liste.php
└── detail.php
Un require à l’intérieur d’une méthode donne à la vue accès aux variables locales de cette méthode : c’est ainsi que $produit arrive dans detail.php, sans variable globale.
<?php
class ProduitModel
{
public function __construct(private PDO $pdo) {}
public function trouver(int $id): array|false
{
$req = $this->pdo->prepare('SELECT p.*, c.nom AS categorie
FROM produits p JOIN categories c ON c.id = p.categorie_id
WHERE p.id = ?');
$req->execute([$id]);
return $req->fetch();
}
}
Le modèle reçoit $pdo dans son constructeur au lieu de créer sa propre connexion : on parle d’injection de dépendance. Tous les modèles partagent ainsi la même connexion.
ProduitModel::trouver sert à la fiche, au panier et à l’API (leçon D.2).index.php./produits/9 au lieu de ?action=detail&id=9).Le serveur (contrôleur) prend la commande, la transmet à la cuisine (modèle) qui prépare le plat avec les ingrédients du stock (la base), puis il apporte l'assiette dressée (vue). Le client ne parle jamais directement au cuisinier.
Toutes les demandes passent par le même guichet d'accueil, qui oriente vers le bon bureau selon le formulaire. Le contrôleur frontal index.php joue ce rôle : il lit ?action= et appelle le bon contrôleur.
La page s'affichera ici.
Le navigateur demandera /index.php au serveur, qui exécutera le PHP et renverra du HTML.
Consignes 0 / 4
Toutes les vues répètent l'en-tête et le pied de page. Écrivez une méthode privée afficher(string $vue, array $donnees) dans le contrôleur, qui inclut views/layout.php ; le gabarit inclut lui-même la vue demandée au milieu.
<?php // dans ProduitController private function afficher(string $vue, array $donnees = []): void { extract($donnees); // ['produits' => ...] devient $produits $contenu = __DIR__ . "/../views/$vue.php"; require __DIR__ . '/../views/layout.php'; } // views/layout.php ?> <!DOCTYPE html> <html lang="fr"> <head><meta charset="UTF-8"><title><?= htmlspecialchars($titre ?? 'Souk Atlas') ?></title></head> <body> <header><a href="index.php">Souk Atlas</a></header> <main><?php require $contenu; ?></main> <footer>Coopérative d'artisans · Fès</footer> </body> </html>
Dans quelle couche (modèle, vue, contrôleur) placez-vous : (a) SELECT * FROM commandes WHERE client_id = ? ; (b) <table> des commandes ; (c) vérifier que le client est connecté avant d'afficher ses commandes ; (d) calculer le total d'une commande ; (e) number_format pour afficher le prix ?
(a) Modèle : tout accès à la base. (b) Vue : le HTML. (c) Contrôleur : il décide de ce qui se passe pour cette requête (rediriger ou continuer). (d) Modèle : c'est une règle métier sur les données (une méthode totalCommande(), ou un SUM en SQL). (e) Vue : c'est de la mise en forme pour l'affichage.
Remplacez le switch de index.php par un tableau $routes = ['liste' => 'liste', 'detail' => 'detail', ...] et un appel dynamique, en répondant 404 si l'action n'est pas dans le tableau.
<?php $routes = [ 'liste' => fn() => $controleur->liste(), 'detail' => fn() => $controleur->detail((int) ($_GET['id'] ?? 0)), 'categorie' => fn() => $controleur->categorie((int) ($_GET['id'] ?? 0)), ]; $action = $_GET['action'] ?? 'liste'; if (!isset($routes[$action])) { http_response_code(404); exit('Page introuvable'); } $routes[$action]();