Quand votre monolithe PHP commence à craquer de toutes parts
\nAvez-vous déjà été dans une situation où le changement de base de données de MySQL à PostgreSQL se transformait en quête d'une semaine ? Ou quand vous deviez ajouter un nouveau client API, mais le code était tellement imbriqué dans le framework qu'il était plus simple de tout réécrire ? Cela vous parle ?
\nLa plupart des développeurs PHP rencontrent tôt ou tard des « spaghettis » dans les contrôleurs et les modèles. Les frameworks comme Laravel ou Symfony offrent des outils pratiques, mais ils imposent également un couplage fort avec eux-mêmes. Il existe une solution — l'architecture Hexagonale (également appelée « ports et adaptateurs », ou architecture hexagonale).
\nDans cet article, nous allons voir comment construire une application en PHP qui ne dépend ni du framework, ni de la base de données, ni des services externes. Vous verrez des exemples de code que vous pouvez appliquer immédiatement dans votre projet. C'est parti !
\n\nQu'est-ce que l'architecture Hexagonale et pourquoi en avez-vous besoin ?
\nImaginez un hexagone. Au centre se trouve votre logique métier (le domaine). Autour, les « ports » (interfaces) par lesquels le monde extérieur communique avec le noyau. Et à l'extérieur, les « adaptateurs » qui implémentent ces ports : contrôleurs, repositories, clients API.
\nL'idée principale : le noyau de l'application ne sait rien du monde extérieur. Il n'importe pas Laravel, ne connaît pas PDO et n'appelle pas curl. Toute la dépendance est dirigée vers l'intérieur — les adaptateurs dépendent des ports, et non l'inverse.
\nLes avantages de cette approche sont évidents :
\n- \n
- Testabilité : la logique métier peut être testée sans base de données ni requêtes HTTP. \n
- Flexibilité : remplacer la base de données, la file d'attente ou le framework revient simplement à écrire un nouvel adaptateur. \n
- Propreté du code : vous ne mélangez pas « quoi faire » avec « comment le faire ». \n
Cela semble magique ? Regardons le code.
\n\nStructure du projet : squelette de base
\nPour l'exemple, prenons un scénario simple : l'inscription d'un utilisateur. Nous devons sauvegarder l'utilisateur dans la base de données et envoyer un email de bienvenue.
\nVoici à quoi ressemblera une structure de répertoires typique :
\nsrc/\n├── Domain/\n│ ├── Entity/\n│ │ └── User.php\n│ ├── Repository/\n│ │ └── UserRepositoryInterface.php\n│ ├── Service/\n│ │ └── UserRegistrationService.php\n│ └── Event/\n│ └── UserRegisteredEvent.php\n├── Infrastructure/\n│ ├── Persistence/\n│ │ └── DoctrineUserRepository.php\n│ ├── Mail/\n│ │ └── SymfonyMailerAdapter.php\n│ └── Controller/\n│ └── RegisterUserController.php\n└── Application/\n └── UseCase/\n └── RegisterUserUseCase.php\nNotez que Domain est la logique métier pure. Infrastructure sont les adaptateurs. Application sont les cas d'utilisation (use cases).
Étape 1 : Le noyau — domaine et ports
\nEntité (Entity)
\nCommençons par le plus simple — l'utilisateur. C'est simplement une classe avec des données, sans dépendances.
\n<?php\nnamespace App\\Domain\\Entity;\n\nclass User\n{\n public function __construct(\n private readonly string $id,\n private string $name,\n private string $email\n ) {}\n\n public function getId(): string\n {\n return $this->id;\n }\n\n public function getName(): string\n {\n return $this->name;\n }\n\n public function getEmail(): string\n {\n return $this->email;\n }\n}\n\nPort — interface du repository
\nDéfinissons maintenant comment nous allons sauvegarder l'utilisateur. C'est un port — une interface qui sera implémentée par un adaptateur.
\n<?php\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Entity\\User;\n\ninterface UserRepositoryInterface\n{\n public function save(User $user): void;\n public function findByEmail(string $email): ?User;\n}\n\nPort — interface du service de messagerie
\nDe même pour l'envoi d'emails :
\n<?php\nnamespace App\\Domain\\Service;\n\nuse App\\