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
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.
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.
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
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.
Le contrôleur maigre, poussé jusqu'au bout
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.
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
Une coquille, une erreur dans ce billet ? Signale-la-moi.