Aller au contenu
M07 · D. Découvrir l'architecture MVC

Organiser le code : l'architecture MVC

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.

1Situer

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.

2Comprendre

Le trajet d'une requête dans une application MVC
Navigateur Tapis berbère index.phproutage ContrôleurProduit Modèletrouver(9) MySQLsouk Vuedetail.php HTML ?action=detail&id=9

Le navigateur demande index.php?action=detail&id=9. Toutes les requêtes arrivent au même fichier, le contrôleur frontal.

1 / 5

Trois rôles, trois dossiers

CoucheCe qu’elle contientCe qu’elle ne contient jamais
Modèleclasses avec PDO, SELECT, INSERT, calculs sur les donnéesecho, HTML, $_GET
VueHTML, <?= ?>, foreach d’affichage, htmlspecialcharsSQL, redirections
Contrôleurlecture 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

Le chemin d’une requête

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.

Le modèle

<?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.

Pourquoi c’est utile

  • Chacun sa partie : l’intégrateur travaille sur les vues, le développeur back-end sur les modèles.
  • Réutilisation : ProduitModel::trouver sert à la fiche, au panier et à l’API (leçon D.2).
  • Une URL, un point d’entrée : la session, la sécurité et la connexion se règlent une seule fois dans index.php.
  • Pas de surprise en arrivant sur un framework : Laravel et Symfony suivent la même logique, avec un routage plus riche (/produits/9 au lieu de ?action=detail&id=9).
Exemple simple

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.

Exemple simple

L'accueil de l'administration

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.

3Manipuler

Le catalogue est organisé en MVC. La liste fonctionne déjà : suivez le chemin d'une requête (index.php, puis le contrôleur, le modèle, la vue), puis complétez la fiche produit et ajoutez la page d'une catégorie.
Pas à pas dans le lab ↗ Cliquez sur Exécuter : PHP 8.3 démarre dans votre navigateur (environ 5 Mo à télécharger, une seule fois).

La page s'affichera ici.

Le navigateur demandera /index.php au serveur, qui exécutera le PHP et renverra du HTML.

Entre le navigateur et le serveur aucun échange pour l'instant

Consignes 0 / 4

  1. index.php?action=detail&id=9 affiche la fiche du Tapis berbère (Textile, 1 200,00 DH)
  2. Un produit qui n'existe pas répond 404 « Produit introuvable »
  3. index.php?action=categorie&id=2 liste seulement les cosmétiques
  4. Une action inconnue répond 404 « Page introuvable »

4Pratiquer

Exercice 1 · Un gabarit commun

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.

Voir le corrigé
<?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>

Exercice 2 · Ranger le bon code au bon endroit

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 ?

Voir le corrigé
(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.

Exercice 3 · Le routage par tableau

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.

Voir le corrigé
<?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]();

5Vérifier

1. Dans MVC, quelle couche contient les requêtes SQL ?
2. Quel est le rôle du contrôleur frontal index.php ?
3. Une vue peut-elle exécuter une requête SQL ?