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.phpdans 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).