L'Architecture Hexagonale : Principes et implémentation en PHP
Découvrez comment l'architecture hexagonale (Ports & Adapters) permet de créer des applications maintenables, testables et indépendantes des frameworks en séparant la logique métier de l'infrastructure.
Qu'est-ce que l'Architecture Hexagonale ?
L'architecture hexagonale, aussi appelée Ports and Adapters, est un pattern architectural créé par Alistair Cockburn. Son objectif principal est de séparer la logique métier du code technique pour créer des applications plus maintenables et testables.
🎯 Principe fondamental : Votre logique métier ne doit dépendre d'aucun framework, base de données ou API externe. Ces éléments sont des détails d'implémentation interchangeables.
Les trois couches de l'architecture
1. Le Domaine (Domain)
Le cœur de l'application contenant :
- Entités : Objets métier avec leur logique
- Value Objects : Objets immuables représentant des concepts
- Services de domaine : Logique métier complexe
- Événements : Notifications de changements d'état
namespace App\Domain\User;
class User
{
private UserId $id;
private Email $email;
private HashedPassword $password;
private \DateTimeImmutable $createdAt;
public function changeEmail(Email $newEmail): void
{
if ($this->email->equals($newEmail)) {
throw new \DomainException('Email is already set');
}
$this->email = $newEmail;
}
public function isActive(): bool
{
return $this->status === UserStatus::ACTIVE;
}
}
2. Les Ports
Interfaces définissant les contrats entre le domaine et l'infrastructure :
namespace App\Application\Port;
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function save(User $user): void;
public function delete(UserId $id): void;
}
interface EmailServiceInterface
{
public function send(EmailMessage $message): void;
}
3. Les Adapters
Implémentations concrètes des ports :
namespace App\Infrastructure\Persistence\Doctrine;
class DoctrineUserRepository implements UserRepositoryInterface
{
public function __construct(
private EntityManagerInterface $em
) {}
public function findById(UserId $id): ?User
{
return $this->em
->getRepository(User::class)
->find($id->value());
}
public function save(User $user): void
{
$this->em->persist($user);
$this->em->flush();
}
}
Les Use Cases (Application Layer)
Les use cases orchestrent la logique métier en utilisant les ports :
namespace App\Application\UseCase\User;
class RegisterUser
{
public function __construct(
private UserRepositoryInterface $userRepository,
private EmailServiceInterface $emailService,
private PasswordHasherInterface $hasher
) {}
public function execute(RegisterUserCommand $command): UserId
{
// Vérifier que l'email n'existe pas
$email = new Email($command->email);
if ($this->userRepository->findByEmail($email)) {
throw new EmailAlreadyExistsException();
}
// Créer l'utilisateur
$user = User::register(
UserId::generate(),
$email,
$this->hasher->hash($command->password)
);
// Sauvegarder
$this->userRepository->save($user);
// Envoyer email de bienvenue
$this->emailService->send(
new WelcomeEmail($user->email())
);
return $user->id();
}
}
Structure du projet
src/
├── Domain/ # Cœur métier
│ ├── User/
│ │ ├── User.php
│ │ ├── UserId.php
│ │ ├── Email.php
│ │ └── UserStatus.php
│ └── Order/
│ ├── Order.php
│ └── OrderItem.php
│
├── Application/ # Use cases et ports
│ ├── UseCase/
│ │ └── User/
│ │ ├── RegisterUser.php
│ │ └── RegisterUserCommand.php
│ └── Port/
│ ├── UserRepositoryInterface.php
│ └── EmailServiceInterface.php
│
├── Infrastructure/ # Adapters
│ ├── Persistence/
│ │ ├── Doctrine/
│ │ │ └── DoctrineUserRepository.php
│ │ └── InMemory/
│ │ └── InMemoryUserRepository.php
│ ├── Email/
│ │ ├── SmtpEmailService.php
│ │ └── MailjetEmailService.php
│ └── Http/
│ └── Controller/
│ └── UserController.php
│
└── UI/ # Interface utilisateur
├── Web/
└── CLI/
Avantages de l'architecture hexagonale
| Avantage | Description |
|---|---|
| 🧪 Testabilité | Tests unitaires sans base de données ni framework |
| 🔄 Flexibilité | Changement facile de base de données ou API |
| 📦 Indépendance | Logique métier isolée des détails techniques |
| 👥 Maintenabilité | Code organisé et facile à comprendre |
Tests unitaires simplifiés
class RegisterUserTest extends TestCase
{
public function testUserCanRegister(): void
{
// Arrange - Pas besoin de base de données !
$userRepo = new InMemoryUserRepository();
$emailService = new FakeEmailService();
$hasher = new FakePasswordHasher();
$useCase = new RegisterUser(
$userRepo,
$emailService,
$hasher
);
// Act
$command = new RegisterUserCommand(
'john@example.com',
'password123'
);
$userId = $useCase->execute($command);
// Assert
$user = $userRepo->findById($userId);
$this->assertNotNull($user);
$this->assertEquals('john@example.com', $user->email()->value());
$this->assertCount(1, $emailService->sentEmails());
}
}
Quand utiliser l'architecture hexagonale ?
Cette architecture est particulièrement adaptée pour :
- Applications métier complexes avec beaucoup de règles
- Projets long terme nécessitant une maintenance sur plusieurs années
- Applications nécessitant plusieurs interfaces (Web, API, CLI, etc.)
- Projets avec tests exigeants où la testabilité est cruciale
⚠️ Attention : Pour des projets simples ou des prototypes, cette architecture peut être sur-dimensionnée. Utilisez-la avec discernement.
Conclusion
L'architecture hexagonale est un investissement qui paie sur le long terme. Elle demande plus de rigueur et de structure au départ, mais garantit une application robuste, testable et évolutive.
En séparant clairement les responsabilités entre domaine, application et infrastructure, vous créez un code plus maintenable et professionnel.