L'architecture hexagonale Symfony isole le domaine métier de l'infrastructure. Moins de régressions, tests unitaires sans base de données, migrations sans réécriture.
La plupart des applications Symfony deviennent difficiles à faire évoluer entre 18 et 36 mois. Non pas parce que Symfony est mal conçu — c'est l'un des frameworks PHP les plus solides — mais parce que sans structure claire, la logique métier finit par se répandre dans les contrôleurs, les entités Doctrine et les templates Twig. Corriger un bug devient une opération à risque. Ajouter une fonctionnalité casse autre chose. L'architecture hexagonale résout ce problème structurellement, dès le premier sprint.
Qu'est-ce que l'architecture hexagonale en Symfony ?
L'architecture hexagonale — aussi appelée Ports & Adapters, introduite par Alistair Cockburn en 2005 — est un pattern qui sépare le domaine métier de toute dépendance technique. Le cœur de l'application ne connaît ni Symfony, ni Doctrine, ni HTTP : il contient uniquement les règles métier. L'infrastructure s'adapte au domaine, pas l'inverse.
Dans un projet Symfony structuré en hexagonale, le code se divise en trois couches distinctes :
- Domain : les entités métier, value objects, règles de gestion et interfaces de repository. Zéro dépendance framework, zéro annotation Doctrine.
- Application : les use cases (commandes et requêtes), les handlers, les DTOs. Orchestre le domaine sans connaître l'infrastructure.
- Infrastructure : les implémentations Symfony, Doctrine, Mailer, contrôleurs HTTP, Command CLI. S'adapte aux ports définis dans le domaine.
Cette séparation n'est pas de la théorie. Elle a des conséquences concrètes sur la vitesse de développement, la qualité des tests et la durée de vie de l'application.
Pourquoi les architectures Symfony classiques accumulent de la dette technique ?
Dans une architecture sans couches claires, la logique métier atterrit là où c'est pratique sur le moment. En pratique, trois anti-patterns reviennent systématiquement :
Le contrôleur fat
La logique de validation, de calcul et de persistance s'accumule dans le contrôleur. Tester cette logique demande de bootstrapper toute la stack Symfony avec ses dépendances. Modifier un comportement métier implique de toucher à des fichiers qui gèrent aussi le routing, le format de réponse et les headers HTTP. Le changement minimal devient un changement à risque.
Le couplage Doctrine-Domain
Quand les entités Doctrine sont directement les objets métier, migrer de MySQL vers PostgreSQL, changer l'ORM ou simplement tester sans base de données devient une opération complexe. L'annotation #[ORM\Entity] dans une classe de domaine, c'est une dépendance d'infrastructure au cœur du métier — une violation structurelle qui s'aggrave à chaque nouvelle entité.
La logique métier dans les templates Twig
Twig est conçu pour l'affichage. Quand des règles de calcul ou de condition métier apparaissent dans les templates — conditions imbriquées sur le statut d'une commande, calcul de prix avec remises — elles ne sont plus testables unitairement, se dupliquent entre les vues, et deviennent invisibles lors des revues de code.
Comment l'architecture hexagonale isole les dépendances en Symfony ?
Le principe central : le domaine définit des interfaces (ports), et l'infrastructure les implémente (adapters). Un exemple concret en PHP 8.2 :
// Domain/Repository/PostRepositoryInterface.php
interface PostRepositoryInterface
{
public function findBySlug(string $slug): ?Post;
public function save(Post $post): void;
}
// Infrastructure/Persistence/Doctrine/Repository/PostRepository.php
final class PostRepository implements PostRepositoryInterface
{
public function findBySlug(string $slug): ?Post
{
$record = $this->doctrineRepo->findOneBy(['slug' => $slug]);
return $record ? $this->mapper->toDomain($record) : null;
}
}
L'application layer (use case) dépend de l'interface — jamais de l'implémentation Doctrine. Pour tester le use case, on injecte un InMemoryPostRepository sans base de données. Si demain on migre l'ORM ou on ajoute une source Redis, seul l'adapter change. Le domaine et les tests restent intacts.
Le flux d'une requête avec cette architecture
- Contrôleur Symfony reçoit la requête HTTP et la désérialise
- Il crée une Command ou une Query (objet DTO immuable)
- Il dispatche sur le Messenger bus (Command bus ou Query bus)
- Le Handler — couche Application — orchestre le Domain
- Le Domain applique les règles métier, soulève des exceptions métier si nécessaire
- L'Infrastructure persiste via le repository ou répond via l'adapter de sortie
Chaque couche a une responsabilité unique. Chaque couche est testable indépendamment. Le contrôleur ne contient que de la glue HTTP — il devient trivial et ne nécessite pas de test unitaire.
Quels gains concrets pour une application métier Symfony ?
| Situation | Architecture standard | Architecture hexagonale |
|---|---|---|
| Ajouter un use case | Modifier contrôleur + service + entité | Créer un Handler, domain inchangé |
| Changer l'ORM | Réécriture partielle du domaine | Seul l'adapter Infrastructure change |
| Tester la logique métier | Tests d'intégration + BDD requis | Tests unitaires purs, sans base de données |
| Corriger un bug métier | Chercher dans controllers, services, entités | Localisé dans le Domain, périmètre clair |
| Onboarder un développeur | Comprendre toute la stack Symfony | Commencer par le Domain — logique pure, lisible |
| Exposer une API REST | Dupliquer ou adapter les services existants | Nouvel adapter HTTP — Domain inchangé |
| Migrer de Symfony 6 à 7 | Risque de régression dans le métier | Seule l'Infrastructure est impactée |
Sur un projet en production depuis 3 ans, la migration vers une architecture hexagonale réduit le temps d'analyse avant une correction de 45 minutes à moins de 15 minutes en moyenne. Les régressions post-modification chutent de manière significative car le périmètre d'impact de chaque changement est clairement délimité.
Les outils Symfony qui facilitent cette architecture
Symfony 7 embarque plusieurs composants qui s'inscrivent naturellement dans une architecture hexagonale :
- Messenger : le bus de commandes et de requêtes, pour dispatcher Command/Query vers leurs Handlers
- DI Container : injection de dépendances par interface — le contrôleur déclare
PostRepositoryInterface, Symfony injectePostRepository - Doctrine XML Mappings : définir les mappings ORM dans des fichiers XML séparés plutôt qu'en annotations dans les entités — le domaine reste libre de toute annotation infrastructure
- PHPStan niveau 9 : analyse statique stricte qui détecte les violations de couches avant la CI
Ces outils ne suffisent pas seuls — la discipline architecturale vient des conventions d'équipe et des revues de code. Mais ils éliminent la friction technique qui pousserait à prendre des raccourcis.
Quand faut-il adopter l'architecture hexagonale ?
Elle n'est pas pertinente pour tout projet. Un site vitrine de 5 pages avec un formulaire de contact n'a pas besoin de Ports & Adapters. En revanche, plusieurs signaux indiquent qu'une application en bénéficierait :
- L'application a plus de 3 entités avec des règles métier interdépendantes
- Elle doit exposer une API en plus d'une interface web
- Elle sera maintenue par plusieurs développeurs sur plusieurs années
- Les tests sont inexistants ou trop lents parce qu'ils requièrent la base de données
- Chaque nouvelle fonctionnalité introduit des régressions dans du code existant
- Un nouveau développeur met plus de 2 semaines à être productif
Pour les projets legacy Symfony déjà en production, la migration se fait progressivement : on isole d'abord les use cases les plus critiques — souvent ceux qui changent le plus souvent — sans tout réécrire d'un coup. Une migration hexagonale complète d'une application de taille moyenne prend entre 3 et 6 mois en parallèle des développements courants.
Ce que KSolution applique sur tous ses projets Symfony
Sur tous les projets KSolution au-delà d'un site vitrine, l'architecture hexagonale est le standard par défaut. Le découpage Domain / Application / Infrastructure est appliqué dès la première ligne de code — pas ajouté 18 mois plus tard quand la refonte devient inévitable.
PHPStan niveau 9, tests unitaires isolés du framework, mappings Doctrine en XML, Command/Query bus via Messenger, zéro annotation framework dans le Domain : ces choix garantissent que l'application est maintenable à 5 ans. Chaque livrable KSolution inclut la documentation de l'architecture et les tests qui en valident les frontières.
Si votre application Symfony actuelle est difficile à faire évoluer ou accumule des régressions, parlons d'un audit technique — la situation est souvent plus simple à corriger qu'il n'y paraît.