Aller au contenu principal

MVC : comment séparer proprement ce qu'on va de toute façon mélanger ensuite.

Apprenez à appliquer correctement le pattern MVC en Symfony. Séparez la logique métier (Model) des contrôleurs et des vues pour un code plus maintenable.

MAJ 7 min de lecture
Sommaire · 8

Le MVC, c'est la question d'entretien à laquelle tout le monde répond, et le pattern que presque personne n'applique à la lettre. Model, View, Controller : trois lettres qu'on récite, trois boîtes qu'on imagine bien rangées, et une réalité qui déborde dès le premier vrai projet. On a tous écrit un contrôleur de 300 lignes en jurant que c'était « du MVC propre ».

Voyons ce que MVC veut vraiment dire, comment il se traduit en Symfony, et surtout pourquoi le fameux Model n'est jamais une seule classe. À la fin, vous saurez où ranger quoi, et pourquoi la séparation est une discipline à tenir, pas un cadeau automatique du framework.

Pour aller plus loin

Les trois lettres

L'idée de MVC tient en une phrase : chaque partie fait son travail et ne vient pas faire celui des autres. Le pattern a été formalisé par Trygve Reenskaug à la fin des années 1970, bien avant le web, pour organiser des interfaces graphiques. Voici les trois rôles.

  • Model : les données et la logique métier. Il sait lire, écrire, calculer, appliquer les règles. C'est là que se passe le travail sérieux.
  • View : la présentation. Il prend ce que le Model a préparé et le met en forme, en HTML le plus souvent.
  • Controller : le coordinateur. Il reçoit la requête, demande au Model, passe le résultat à la View. Il ne calcule pas lui-même.

Sur le papier, tout est net. Le problème commence quand on ouvre un vrai projet Symfony et qu'on cherche le fichier Model.php. Il n'existe pas.

MVC en Symfony, la version manuel scolaire

Voici l'exemple qu'on montre en cours : un contrôleur qui récupère un article et le passe à un gabarit.

PHP
final class ArticleController extends AbstractController
{
    #[Route('/articles/{id}', name: 'article_show')]
    public function show(int $id, ArticleRepository $repository): Response
    {
        $article = $repository->find($id);

        return $this->render('article/show.html.twig', ['article' => $article]);
    }
}

Et la View correspondante, en Twig :

Twig
{# templates/article/show.html.twig #}
<h1>{{ article.title }}</h1>
<div>{{ article.content }}</div>

C'est correct, c'est lisible, et c'est ce qu'on répond en entretien. Mais ce n'est qu'une moitié de l'histoire. Regardez le Model : il est passé où ?

Où le Model explose

Le grand non-dit du MVC, c'est que le Model n'est pas une classe. Dans une application Symfony sérieuse, il se répartit sur au moins trois rôles distincts.

PHP
// L'Entity : la forme de la donnée
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
    public string $title;
    public string $content;
}

// Le Repository : les requêtes vers la base
class ArticleRepository extends ServiceEntityRepository
{
    public function findPublishedBySlug(string $slug): ?Article { /* ... */ }
}

// Le Service : la logique métier
final readonly class ArticleService
{
    public function getReadableArticle(int $id): Article { /* règles, 404... */ }
}

L'entité décrit la donnée, le repository sait la chercher, le service porte les règles. Le « M » de MVC, c'est ces trois-là ensemble. Confondre les trois, c'est exactement comme ça qu'on se retrouve avec de la logique métier écrite dans une requête Doctrine, ou une entité qui envoie des e-mails.

Où le Controller doit rester maigre

Le Controller est le rôle qu'on maltraite le plus. On y met une validation, puis un calcul, puis un appel d'API, et de fil en aiguille il redevient le fourre-tout que MVC était censé éviter. La règle tient en une image : le contrôleur passe des plats, il ne cuisine pas.

Sur ce blog, la discipline va plus loin. Le contrôleur ne parle même pas au repository directement, il passe par un service, et un outil vérifie cette frontière au build. La version scolaire ci-dessus deviendrait donc :

Pour aller plus loin

PHP
final class ArticleShowAction extends AbstractController
{
    #[Route('/articles/{id}', name: self::class)]
    public function __invoke(int $id, ArticleService $articleService): Response
    {
        $article = $articleService->getReadableArticle($id); // 404 gérée dans le service

        return $this->render('article/show.html.twig', ['article' => $article]);
    }
}

Le contrôleur reçoit, délègue, retourne. Rien d'autre. C'est le point de départ d'un pattern plus strict, l'ADR, qui pousse l'idée jusqu'au bout.

Le contrôleur maigre, poussé jusqu'au bout

Où la View n'est pas si passive

On imagine la View comme un simple gabarit qui affiche des variables. En pratique, Twig contient de la logique de présentation : des boucles, des conditions, des filtres de formatage, des composants réutilisables. La frontière saute quand on y glisse de la logique métier, par exemple un calcul de remise ou un accès direct à la base depuis le template. La View a le droit de décider comment afficher, pas quoi calculer.

Un point pour la sérénité : Twig échappe les variables par défaut. La View protège donc contre la majorité des injections HTML sans que vous ayez à y penser, tant que vous ne forcez pas le rendu brut.

La séparation qu'on va mélanger quand même

Voilà le cœur du titre. MVC ne garantit rien. Le framework vous donne les dossiers et les conventions, mais rien ne vous empêche d'écrire une requête SQL dans un contrôleur, une règle métier dans un template, ou d'entasser quinze responsabilités dans une entité. La séparation est un objectif que vous tenez, pas une propriété que le code possède tout seul.

Les fuites les plus courantes portent toutes un nom. Le fat controller, qui accumule la logique au lieu de déléguer. Le anemic model, une entité réduite à des getters pendant que la logique se disperse ailleurs. La logique dans la vue, ce calcul « juste pour aujourd'hui » qui campe dans le template deux ans plus tard. Les connaître, c'est déjà savoir où elles vont apparaître.

MVC, un vocabulaire commun plus qu'un plan de maison

MVC n'est pas l'architecture littérale de votre application, c'est le vocabulaire partagé qui permet d'en parler. Sa vraie valeur n'est pas d'imposer trois dossiers, c'est de donner un nom à chaque responsabilité, pour que le jour où ça casse, on sache exactement où regarder : dans le Model si la donnée est fausse, dans la View si l'affichage déraille, dans le Controller si le mauvais service est appelé.

La prochaine fois qu'on vous dira « c'est du MVC propre », posez la seule question qui compte : où vit la logique métier ? La réponse en dit plus long sur un projet que les trois lettres réunies.

MVC, au-delà de l'acronyme

En Symfony, à quoi correspond le « Model » de MVC ?

Voir la réponse

Bonne réponse : Plusieurs classes : l'entité, le repository et souvent un service

Le Model se répartit : l'entité décrit la donnée, le repository la cherche, le service porte les règles métier.
Où doit vivre une logique métier un peu complexe ?

Voir la réponse

Bonne réponse : Dans un service

Le contrôleur coordonne, la vue affiche. La logique métier va dans un service, réutilisable et testable.
MVC garantit-il une séparation propre des responsabilités ?

Voir la réponse

Bonne réponse : Non : c'est une discipline à tenir, rien n'empêche un fat controller

Le framework donne les dossiers et les conventions. Tenir la séparation reste un choix, revue de code après revue de code.

Une coquille, une erreur dans ce billet ? Signale-la-moi.

Vous aimerez aussi

Activez uniquement ce que vous souhaitez. Vos choix sont conservés 6 mois.

Strictement nécessaires

Indispensables au fonctionnement du site (session, sécurité, préférence d'affichage). Aucune donnée n'est partagée à des tiers et aucun consentement n'est requis.

Toujours actif

Mesure d'audience

Statistiques via Google Analytics (GA4) : pages vues, source du trafic, navigateur et interactions clés. Dépose des cookies de mesure, activés seulement avec votre accord (Consent Mode). Sans publicité ciblée, sans Google Signals, sans partage commercial.

Contenus externes

Affiche les GIF animés hébergés par Giphy (CDN aux États-Unis). À l'affichage d'un GIF, votre adresse IP et votre navigateur sont transmis à Giphy. Sans votre accord, les GIF ne s'affichent pas.