ahmed-bhs/hexagonal-maker-bundle
A Port is an interface defined in the Domain layer that declares what the domain needs from the outside world.
Domain defines: "I need to save users" → UserRepositoryInterface (Port)
Infrastructure provides: "Here's how" → DoctrineUserRepository (Adapter)
Domain/Port/In/ (driving) or Domain/Port/Out/ (driven)Domain/Port/
├── In/ # Input/Driving Ports (Primary)
│ └── CreateUserUseCaseInterface.php # Implemented by Application layer
└── Out/ # Output/Driven Ports (Secondary)
└── UserRepositoryInterface.php # Implemented by Infrastructure layer
Definition: An interface that defines what the application offers to the outside world.
Question to ask: "Who initiates the action?" → If the outside world initiates, it's a Port In.
Analogy: The front door of your house. Visitors knock on it to request something from you.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Controller │ ──────> │ Port/In │ ──────> │ UseCase │
│ (UI) │ calls │ (interface) │ impl by │ (App) │
└─────────────┘ └─────────────┘ └─────────────┘
OUTSIDE DOMAIN APPLICATION
(calls us) (contract) (does the work)
Characteristics:
Domain/Port/In/Examples:
// "The application CAN register users"
interface RegisterUserUseCaseInterface {
public function execute(RegisterUserCommand $command): UserId;
}
// "The application CAN place orders"
interface PlaceOrderUseCaseInterface {
public function execute(PlaceOrderCommand $command): OrderId;
}
// "The application CAN cancel subscriptions"
interface CancelSubscriptionUseCaseInterface {
public function execute(CancelSubscriptionCommand $command): void;
}
Definition: An interface that defines what the application needs from the outside world.
Question to ask: "Who initiates the action?" → If the application initiates, it's a Port Out.
Analogy: The back door of your house. You go through it to get something you need (groceries, mail, etc.).
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UseCase │ ──────> │ Port/Out │ ──────> │ Adapter │
│ (App) │ uses │ (interface) │ impl by │ (Infra) │
└─────────────┘ └─────────────┘ └─────────────┘
APPLICATION DOMAIN INFRASTRUCTURE
(needs sth) (contract) (provides it)
Characteristics:
Domain/Port/In/Examples:
// "The application NEEDS to persist users"
interface UserRepositoryInterface {
public function save(User $user): void;
public function findById(UserId $id): ?User;
}
// "The application NEEDS to calculate taxes"
interface TaxCalculatorInterface {
public function calculate(Money $amount, Country $country): Money;
}
// "The application NEEDS to check stock"
interface InventoryCheckerInterface {
public function isAvailable(ProductId $id, int $quantity): bool;
}
WHO CALLS? WHO IMPLEMENTS?
┌─────────┐
│ UI │ (Controller, CLI)
└────┬────┘
│ calls
▼
╔═════════════╗ ┌─────────────┐
║ Port/In ║ ◄───────────────│ Application │
║ (interface) ║ implemented │ (UseCase) │
╚═════════════╝ by └──────┬──────┘
│ uses
▼
╔═════════════╗
┌─────────────┐ implemented ║ Port/Out ║
│ Infra │ ───────────────>║ (interface) ║
│ (Adapter) │ by ╚═════════════╝
└─────────────┘
┌─────────────────────────────────────────────────────────┐
│ 💎 DOMAIN (Hexagon) │
│ │
│ Port/In/ (What the application CAN DO) │
│ ├── RegisterUserUseCaseInterface │
│ ├── PlaceOrderUseCaseInterface │
│ ├── CancelSubscriptionUseCaseInterface │
│ └── ApplyDiscountUseCaseInterface │
│ │
│ Port/Out/ (What the application NEEDS) │
│ ├── UserRepositoryInterface # Persistence │
│ ├── PricingServiceInterface # Price calc │
│ ├── TaxCalculatorInterface # VAT calc │
│ ├── InventoryCheckerInterface # Stock check │
│ ├── FraudDetectionInterface # Anti-fraud │
│ ├── LoyaltyPointsServiceInterface # Loyalty points │
│ ├── ShippingCostCalculatorInterface # Shipping fees │
│ └── InvoiceGeneratorInterface # Invoicing │
│ │
└─────────────────────────────────────────────────────────┘
▲ ▲
│ │
┌──────────┴────────┐ ┌───────┴───────────────┐
│ APPLICATION │ │ INFRASTRUCTURE │
│ (implements │ │ (implements │
│ Port/In) │ │ Port/Out) │
│ │ │ │
│ PlaceOrderUseCase │ │ StripePricingService │
│ RegisterUserCase │ │ TaxJarCalculator │
│ │ │ WarehouseInventory │
│ Orchestrates: │ │ SiftFraudDetection │
│ - check stock │ │ ColissimoShipping │
│ - calc price │ │ DoctrineRepositories │
│ - detect fraud │ │ │
└───────────────────┘ └───────────────────────┘
Dependency flow:
✅ GOOD:
interface UserRepositoryInterface // Clear: manages User entities
interface OrderRepositoryInterface // Clear: manages Order entities
interface ProductRepositoryInterface // Clear: manages Product entities
❌ BAD:
interface UserDAO // Technical term (Data Access Object)
interface UserPersistence // Vague
interface IUserRepository // Hungarian notation (avoid "I" prefix)
interface UserRepositoryPort // Redundant suffix
✅ GOOD:
interface EmailSenderInterface // Clear capability
interface PaymentProcessorInterface // Clear responsibility
interface NotificationServiceInterface // Clear purpose
❌ BAD:
interface EmailService // Too vague
interface IEmailSender // Hungarian notation
interface SMTPEmailSender // Implementation detail leaked!
✅ GOOD:
interface UserQueryInterface // Clear: read operations for Users
interface OrderQueryInterface // Clear: read operations for Orders
interface ProductCatalogQueryInterface // Clear: specific read concern
❌ BAD:
interface UserReader // Unclear
interface GetUserQuery // Not a capability, but an action
"Clients should not be forced to depend on methods they do not use."
❌ BAD: God Interface
interface UserRepositoryInterface
{
// Read methods
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;
// Write methods
public function save(User $user): void;
public function delete(User $user): void;
// Statistics methods
public function countUsers(): int;
public function countActiveUsers(): int;
// Admin methods
public function purgeInactiveUsers(): void;
public function exportUsersToCSV(): string;
// Notification methods
public function findUsersToNotify(): array;
}
Problems:
✅ GOOD: Segregated by Responsibility
// Write operations
interface UserRepositoryInterface
{
public function save(User $user): void;
public function delete(User $user): void;
public function existsByEmail(string $email): bool;
}
// Read operations (CQRS pattern)
interface UserQueryInterface
{
public function findById(UserId $id): ?User;
public function findByEmail(string $email): ?User;
public function findActiveUsers(): array;
}
// Admin operations
interface UserAdminInterface
{
public function purgeInactiveUsers(): void;
public function countUsers(): int;
}
// Notification operations
interface UserNotificationQueryInterface
{
public function findUsersToNotify(): array;
}
Benefits:
✅ Keep together when methods are always used together:
// GOOD: These methods logically belong together
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
❌ Split when methods serve different use cases:
// BAD: findPendingOrders is specific to a background job
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // ❌ Different concern!
}
// GOOD: Separate query interface
interface OrderQueryInterface
{
public function findPendingOrders(): array;
}
Guidelines
✅ GOOD: Domain Language
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function findPendingOrders(): array; // Business concept
}
❌ BAD: Technical Language
interface OrderRepositoryInterface
{
public function persist(Order $order): void; // Technical (SQL term)
public function selectById(OrderId $id): ?Order; // Technical (SQL term)
public function queryByStatusPending(): array; // Technical implementation detail
}
✅ GOOD: Domain Objects
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function findActiveUsers(): array; // array<User>
}
❌ BAD: Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?array; // array is not type-safe
public function findActiveUsers(): array; // array<what?>
}
Use PHPDoc for clarity:
interface UserRepositoryInterface
{
/**
* [@return](https://github.com/return) array<User>
*/
public function findActiveUsers(): array;
}
✅ GOOD: Value Objects
interface UserRepositoryInterface
{
public function findById(UserId $id): ?User;
public function existsByEmail(Email $email): bool;
}
❌ BAD: Primitives
interface UserRepositoryInterface
{
public function findById(string $id): ?User;
public function existsByEmail(string $email): bool; // Loses domain validation
}
Why? Value objects ensure validation happens at the boundary, not in the adapter.
Method names should read like natural language.
✅ GOOD: Readable
if ($this->users->existsByEmail($email)) {
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->findPendingOrders();
❌ BAD: Unclear
if ($this->users->checkEmail($email)) { // Check what about email?
throw new EmailAlreadyExistsException();
}
$orders = $this->orders->getPending(); // Get pending what?
✅ GOOD: Implementation-Agnostic
interface NotificationServiceInterface
{
public function send(Notification $notification): void;
}
❌ BAD: Leaks Implementation
interface NotificationServiceInterface
{
public function sendViaSmtp(Notification $notification): void; // ❌ SMTP is implementation detail
public function sendViaSendGrid(Notification $notification): void; // ❌ SendGrid is implementation detail
}
Why? Port should describe "what", not "how". Implementation can change without changing the port.
Ports should be easy to mock/stub.
✅ GOOD: Simple, Testable
interface EmailSenderInterface
{
public function send(Email $email): void;
}
// Test with in-memory fake
class InMemoryEmailSender implements EmailSenderInterface
{
private array $sentEmails = [];
public function send(Email $email): void
{
$this->sentEmails[] = $email;
}
public function getSentEmails(): array
{
return $this->sentEmails;
}
}
❌ BAD: Hard to Test
interface EmailSenderInterface
{
public function send(
Email $email,
EmailConfiguration $config,
TransportOptions $transport,
RetryPolicy $retry
): SendResult;
}
// Test requires complex setup with many dependencies
Purpose: Manage aggregate root lifecycle (CRUD).
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
public function delete(Order $order): void;
}
Key Points:
save, not persist)Purpose: Optimized read operations, may return DTOs instead of entities.
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;
}
Key Points:
Purpose: Communicate with external systems (email, payment, etc.).
interface PaymentProcessorInterface
{
public function charge(PaymentRequest $request): PaymentResult;
public function refund(RefundRequest $request): RefundResult;
}
Key Points:
Purpose: Publish domain events.
interface EventDispatcherInterface
{
public function dispatch(DomainEvent $event): void;
}
Key Points:
Purpose: Build complex queries dynamically.
interface UserSpecificationInterface
{
public function matching(Specification $spec): array;
}
// Usage
$activeAdmins = $this->users->matching(
new AndSpecification(
new IsActiveSpecification(),
new HasRoleSpecification(Role::ADMIN)
)
);
Key Points:
❌ AVOID:
interface GenericRepositoryInterface
{
public function save(object $entity): void;
public function findById(string $id): ?object;
public function findAll(): array;
}
Problems:
object and string are too generic)✅ BETTER:
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;
}
❌ AVOID:
interface OrderRepositoryInterface
{
public function save(Order $order): void;
// ❌ Business logic leaked into repository!
public function cancelOrder(OrderId $id): void;
public function shipOrder(OrderId $id, Address $address): void;
}
Problem: Repository should manage persistence, not execute business logic.
✅ BETTER:
// Repository: persistence only
interface OrderRepositoryInterface
{
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
// Business logic in handlers
class CancelOrderHandler
{
public function __invoke(CancelOrderCommand $command): void
{
$order = $this->orders->findById($command->orderId);
$order->cancel(); // Business logic in entity
$this->orders->save($order);
}
}
❌ AVOID:
interface UserRepositoryInterface
{
/**
* [@return](https://github.com/return) array<array{id: string, email: string, name: string}>
*/
public function findAllUsers(): array;
}
Problem: Array shapes are error-prone and not type-safe.
✅ BETTER:
interface UserQueryInterface
{
/**
* [@return](https://github.com/return) array<UserListDTO>
*/
public function findAllUsers(): array;
}
final readonly class UserListDTO
{
public function __construct(
public string $id,
public string $email,
public string $name,
) {}
}
❌ AVOID:
use Doctrine\ORM\EntityManagerInterface;
interface UserRepositoryInterface
{
public function getEntityManager(): EntityManagerInterface; // ❌ Leaks infrastructure!
}
Problem: Domain now depends on Doctrine.
✅ BETTER:
interface UserRepositoryInterface
{
public function save(User $user): void;
public function findById(UserId $id): ?User;
// No mention of Doctrine, EntityManager, or any framework
}
| Port Out | Business Need | Possible Implementations |
|---|---|---|
PricingServiceInterface |
Calculate final price (promos, B2B, etc.) | StripePricing, CustomPricingEngine |
TaxCalculatorInterface |
Calculate VAT by country/product | TaxJarAPI, GovernmentTaxAPI |
InventoryCheckerInterface |
Check stock availability | WarehouseAPI, ERPConnector |
FraudDetectionInterface |
Detect suspicious orders | SiftScience, Signifyd |
LoyaltyPointsServiceInterface |
Manage loyalty points | ZendeskLoyalty, InternalSystem |
ShippingCostCalculatorInterface |
Calculate shipping fees | Colissimo, UPS, FedEx |
CreditCheckInterface |
Check B2B.... |
How can I help you explore Laravel packages today?