Aller au contenu principal

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

Symfony : évitez le chaos du pattern MVC mal appliqué. Découvrez où placer la logique métier pour un code propre et maintenable, loin des exemples scolaires.

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.

À lire aussi : Anatomie d’un projet Symfony

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.

En entretien, ce qu'on attend vraiment

Réciter « Model, View, Controller » ne prouve pas grand-chose. La question de suivi qui sépare, c'est : où va la logique métier ? La bonne réponse est « dans un service », pas dans le contrôleur ni dans la vue.

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.

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 :

{# 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.

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

Le Model n'est pas un fichier

Ne cherchez pas Model.php dans un projet Symfony, il n'existe pas. Le « M » se répartit entre l'entité (la donnée), le repository (les requêtes) et le service (les règles). Confondre les trois est la première source de code emmêlé.

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 :

À lire aussi : L'injection de dépendance, ou comment être fainéant avec élégance

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.

À lire aussi : Arrêtez les God Controllers, passez à l'ADR

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.

Les trois fuites à surveiller

Le fat controller, qui cuisine au lieu de passer les plats. L'anemic model, coquille de getters pendant que la logique s'éparpille. Le calcul « juste pour aujourd'hui » qui s'installe dans un template et y reste deux ans. Aucune n'est bloquée par le framework : c'est à vous de les repérer en revue.

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 ?

  • Plusieurs classes : l'entité, le repository et souvent un service

  • Une seule classe Model.php

  • Le template Twig

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 ?

  • Dans un service

  • Dans le contrôleur, c'est son rôle

  • Dans le template, au plus près de l'affichage

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 ?

  • Non : c'est une discipline à tenir, rien n'empêche un fat controller

  • Oui, le framework l'impose

  • Oui, automatiquement dès qu'on utilise Symfony

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

Ce billet est publié sous licence Creative Commons BY-NC-SA 4.0 (attribution, pas d'usage commercial, partage dans les mêmes conditions).

Vous aimerez aussi

  1. PHP · 10 min ·

    Fibers PHP : anatomie d'une primitive mal aimée

    Les Fibers PHP, bien que peu utilisées directement, sont le moteur des bibliothèques async modernes comme Amphp. Découvrez leur fonctionnement bas niveau.

  2. PHP · 7 min ·

    Recommandations de contenu avec Meilisearch

    Meilisearch appelle lui-même le fournisseur d'embeddings au push. Ce que ça change, ce que /similar sait faire, et pourquoi il faut quand même un repli.

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.