ahmed-bhs/hexagonal-maker-bundle
Un Port est une interface définie dans la couche Domaine qui déclare ce que le domaine a besoin du monde extérieur.
Le Domaine définit : "J'ai besoin de sauvegarder des utilisateurs" → UserRepositoryInterface (Port)
L'Infrastructure fournit : "Voici comment" → DoctrineUserRepository (Adaptateur)
Domain/Port/Infrastructure/✅ BON :
interface UserRepositoryInterface // Clair : gère les entités User
interface OrderRepositoryInterface // Clair : gère les entités Order
interface ProductRepositoryInterface // Clair : gère les entités Product
❌ MAUVAIS :
interface UserDAO // Terme technique (Data Access Object)
interface UserPersistence // Vague
interface IUserRepository // Notation hongroise (éviter préfixe "I")
interface UserRepositoryPort // Suffixe redondant
✅ BON :
interface EmailSenderInterface // Capacité claire
interface PaymentProcessorInterface // Responsabilité claire
interface NotificationServiceInterface // Objectif clair
❌ MAUVAIS :
interface EmailService // Trop vague
interface IEmailSender // Notation hongroise
interface SMTPEmailSender // Détail d'implémentation fuite !
✅ BON :
interface UserQueryInterface // Clair : opérations lecture pour Users
interface OrderQueryInterface // Clair : opérations lecture pour Orders
interface ProductCatalogQueryInterface // Clair : préoccupation lecture spécifique
❌ MAUVAIS :
interface UserReader // Peu clair
interface GetUserQuery // Pas une capacité, mais une action
"Les clients ne devraient pas être forcés de dépendre de méthodes qu'ils n'utilisent pas."
❌ MAUVAIS : Interface Dieu
interface UserRepositoryInterface
{
// Méthodes lecture
public function findById(UserId $id): ?User;
public function findByEmail(string $email): ?User;
public function findAll(): array;
public function findActiveUsers(): array;
public function findUsersByRole(string $role): array;
public function searchUsers(string $query): array;
// Méthodes écriture
public function save(User $user): void;
public function delete(User $user): void;
// Méthodes statistiques
public function countUsers(): int;
public function countActiveUsers(): int;
// Méthodes admin
public function purgeInactiveUsers(): void;
public function exportUsersToCSV(): string;
// Méthodes notification
public function findUsersToNotify(): array;
}
Problèmes :
✅ BON : Ségrégé par Responsabilité
// Opérations écriture
interface UserRepositoryInterface
{
public function save(User $user): void;
public function delete(User $user): void;
public function existsByEmail(string $email): bool;
}
// Opérations lecture (pattern CQRS)
interface UserQueryInterface
{
public function findById(UserId $id): ?User;
public function findByEmail(string $email): ?User;
public function findActiveUsers(): array;
}
// Opérations admin
interface UserAdminInterface
{
public function purgeInactiveUsers(): void;
public function countUsers(): int;
}
// Opérations notification
interface UserNotificationQueryInterface
{
public function findUsersToNotify(): array;
}
Bénéfices :
✅ Garder ensemble quand les méthodes sont toujours utilisées ensemble :
// BON : Ces méthodes appartiennent logiquement ensemble
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
❌ Diviser quand les méthodes servent différents cas d'usage :
// MAUVAIS : findPendingOrders est spécifique à un job en arrière-plan
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // ❌ Préoccupation différente !
}
// BON : Interface query séparée
interface OrderQueryInterface
{
public function findPendingOrders(): array;
}
✅ BON : Langage Domaine
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // Concept métier
}
❌ MAUVAIS : Langage Technique
interface OrderRepositoryInterface
{
public function persist(Order $order): void; // Technique (terme SQL)
public function selectById(OrderId $id): ?Order; // Technique (terme SQL)
public function queryByStatusPending(): array; // Détail d'implémentation technique
}
✅ BON : Objets Domaine
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function findActiveUsers(): array; // array<User>
}
❌ MAUVAIS : Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?array; // array n'est pas type-safe
public function findActiveUsers(): array; // array<quoi?>
}
Utiliser PHPDoc pour la clarté :
interface UserRepositoryInterface
{
/**
* [@return](https://github.com/return) array<User>
*/
public function findActiveUsers(): array;
}
✅ BON : Value Objects
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function existsByEmail(Email $email): bool;
}
❌ MAUVAIS : Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?User;
public function existsByEmail(string $email): bool; // Perd la validation domaine
}
Pourquoi ? Les value objects assurent que la validation se produit à la frontière, pas dans l'adaptateur.
Les noms de méthode doivent se lire comme du langage naturel.
✅ BON : Lisible
if ($this->users->existsByEmail($email)) {
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->findPendingOrders();
❌ MAUVAIS : Peu Clair
if ($this->users->checkEmail($email)) { // Vérifier quoi sur l'email ?
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->getPending(); // Obtenir pending quoi ?
✅ BON : Agnostique à l'Implémentation
interface NotificationServiceInterface
{
public function send(Notification $notification): void;
}
❌ MAUVAIS : Fuite d'Implémentation
interface NotificationServiceInterface
{
public function sendViaSmtp(Notification $notification): void; // ❌ SMTP est détail d'implémentation
public function sendViaSendGrid(Notification $notification): void; // ❌ SendGrid est détail d'implémentation
}
Pourquoi ? Le port doit décrire "quoi", pas "comment". L'implémentation peut changer sans changer le port.
Les ports doivent être faciles à mocker/stub.
✅ BON : Simple, Testable
interface EmailSenderInterface
{
public function send(Email $email): void;
}
// Test avec fake en mémoire
class InMemoryEmailSender implements EmailSenderInterface
{
private array $sentEmails = [];
public function send(Email $email): void
{
$this->sentEmails[] = $email;
}
public function getSentEmails(): array
{
return $this->sentEmails;
}
}
❌ MAUVAIS : Difficile à Tester
interface EmailSenderInterface
{
public function send(
Email $email,
EmailConfiguration $config,
TransportOptions $transport,
RetryPolicy $retry
): SendResult;
}
// Test nécessite configuration complexe avec nombreuses dépendances
Objectif : Gérer le cycle de vie de la racine d'agrégat (CRUD).
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
Points Clés :
save, pas persist)Objectif : Opérations de lecture optimisées, peut retourner des DTOs au lieu d'entités.
interface ProductCatalogQueryInterface
{
/**
* [@return](https://github.com/return) array<ProductListDTO>
*/
public function findAvailableProducts(int $limit, int $offset): array;
public function findProductById(ProductId $id): ?ProductDetailDTO;
public function searchProducts(string $query): array;
}
Points Clés :
Objectif : Communiquer avec des systèmes externes (email, paiement, etc.).
interface PaymentProcessorInterface
{
public function charge(PaymentRequest $request): PaymentResult;
public function refund(RefundRequest $request): RefundResult;
}
Points Clés :
Objectif : Publier des événements domaine.
interface EventDispatcherInterface
{
public function dispatch(DomainEvent $event): void;
}
Points Clés :
❌ À ÉVITER :
interface GenericRepositoryInterface
{
public function save(object $entity): void;
public function findById(string $id): ?object;
public function findAll(): array;
}
Problèmes :
object et string sont trop génériques)✅ MIEUX :
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
}
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
❌ À ÉVITER :
interface OrderRepositoryInterface
{
public function save(Order $order): void;
// ❌ Logique métier fuitée dans le repository !
public function cancelOrder(OrderId $id): void;
public function shipOrder(OrderId $id, Address $address): void;
}
Problème : Repository devrait gérer la persistance, pas exécuter la logique métier.
✅ MIEUX :
// Repository : persistance uniquement
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
// Logique métier dans les handlers
class CancelOrderHandler
{
public function __invoke(CancelOrderCommand $command): void
{
$order = $this->orders->findById($command->orderId);
$order->cancel(); // Logique métier dans l'entité
$this->orders->save($order);
}
}
❌ À ÉVITER :
use Doctrine\ORM\EntityManagerInterface;
interface UserRepositoryInterface
{
public function getEntityManager(): EntityManagerInterface; // ❌ Fuite infrastructure !
}
Problème : Le domaine dépend maintenant de Doctrine.
✅ MIEUX :
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
// Aucune mention de Doctrine, EntityManager, ou framework
}
// Opérations écriture
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function nextOrderNumber(): OrderNumber;
}
// Opérations lecture (optimisées pour affichage)
interface OrderQueryInterface
{
/**
* [@return](https://github.com/return) array<OrderListDTO>
*/
public function findOrdersByCustomer(CustomerId $customerId, int $limit, int $offset): array;
public function findOrderDetails(OrderId $id): ?OrderDetailDTO;
/**
* [@return](https://github.com/return) array<OrderListDTO>
*/
public function findRecentOrders(int $limit): array;
}
// Service paiement externe
interface PaymentProcessorInterface
{
public function charge(PaymentRequest $request): PaymentResult;
public function refund(RefundRequest $request): RefundResult;
public function getTransactionStatus(TransactionId $id): TransactionStatus;
}
// Gestion inventaire
interface InventoryServiceInterface
{
public function reserveStock(ProductId $productId, int $quantity): void;
public function releaseStock(ProductId $productId, int $quantity): void;
public function checkAvailability(ProductId $productId): int;
}
// Persistance utilisateur
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
public function findByEmail(Email $email): ?User;
public function existsByEmail(Email $email): bool;
}
// Hachage mot de passe (service externe)
interface PasswordHasherInterface
{
public function hash(string $plaintext): string;
public function verify(string $plaintext, string $hash): bool;
}
// Notifications email
interface EmailSenderInterface
{
public function send(Email $email): void;
}
// Génération token
interface TokenGeneratorInterface
{
public function generate(): string;
}
Lors de la conception d'un port, demandez-vous :
Domain/Port/) ?| Principe | Directive |
|---|---|
| Nommage | Utiliser langage domaine, éviter termes techniques |
| Ségrégation | Diviser interfaces par responsabilité (ISP) |
| Types | Accepter/retourner objets domaine, pas primitives |
| Clarté | Méthodes doivent se lire comme langage naturel |
| Abstraction | Cacher complètement détails d'implémentation |
| Testabilité | Facile à mocker avec fakes en mémoire |
| Localisation | Toujours dans Domain/Port/, jamais dans Infrastructure |
Suivant : Primary vs Secondary Adapters →
How can I help you explore Laravel packages today?