Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Hexagonal Maker Bundle Laravel Package

ahmed-bhs/hexagonal-maker-bundle

View on GitHub
Deep Wiki
Context7

layout: default title: Port Interface Design Principles parent: Advanced Topics nav_order: 12 lang: en lang_ref: fr/advanced/principes-conception-ports.md

Port Interface Design Principles

Table of Contents

  1. What is a Port?
  2. Naming Conventions
  3. Interface Segregation Principle (ISP)
  4. Method Design Guidelines
  5. Common Port Patterns
  6. Anti-Patterns to Avoid
  7. Real-World Examples

What is a Port?

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)

Key Characteristics

  • Defined in Domain - Lives in Domain/Port/In/ (driving) or Domain/Port/Out/ (driven)
  • Input Ports (In) - Define what the application offers, implemented by Application layer
  • Output Ports (Out) - Define what the application needs, implemented by Infrastructure layer
  • Expresses Business Intent - Uses domain language, not technical language
  • No Implementation Details - No mention of Doctrine, MySQL, HTTP, etc.

Port Types

Domain/Port/
├── In/                                    # Input/Driving Ports (Primary)
│   └── CreateUserUseCaseInterface.php     # Implemented by Application layer
└── Out/                                   # Output/Driven Ports (Secondary)
    └── UserRepositoryInterface.php        # Implemented by Infrastructure layer

Port In vs Port Out - Complete Explanation


Port In (Driving / Primary Port)

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:

  • Defined in Domain/Port/In/
  • Implemented by Application layer (Use Cases)
  • Called by UI layer (Controllers, CLI, API)
  • Represents a business capability the application exposes

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;
}

Port Out (Driven / Secondary Port)

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:

  • Defined in Domain/Port/In/
  • Implemented by Infrastructure layer (Adapters)
  • Used by Application layer (Use Cases)
  • Represents a dependency the application requires

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;
}

Visual Summary

          WHO CALLS?                    WHO IMPLEMENTS?

          ┌─────────┐
          │   UI    │ (Controller, CLI)
          └────┬────┘
               │ calls
               ▼
        ╔═════════════╗                 ┌─────────────┐
        ║  Port/In    ║ ◄───────────────│ Application │
        ║ (interface) ║   implemented   │  (UseCase)  │
        ╚═════════════╝   by            └──────┬──────┘
                                               │ uses
                                               ▼
                                        ╔═════════════╗
        ┌─────────────┐  implemented    ║  Port/Out   ║
        │   Infra     │ ───────────────>║ (interface) ║
        │  (Adapter)  │  by             ╚═════════════╝
        └─────────────┘

Architecture Overview

        ┌─────────────────────────────────────────────────────────┐
        │                   💎 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:

  • Application → depends on → Domain (uses ports)
  • Infrastructure → depends on → Domain (implements Port/Out)
  • UI → depends on → Application (calls use cases via Port/In)

Naming Conventions

Repository Ports

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

Service Ports

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!

Query Ports (CQRS)

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

Interface Segregation Principle (ISP)

"Clients should not be forced to depend on methods they do not use."

The Problem: Fat Interfaces

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:

  • Handler that only saves users depends on 15 methods it doesn't need
  • Hard to test (must mock 15 methods)
  • Hard to implement (adapter must implement everything)
  • Violates Single Responsibility Principle

The Solution: Segregated Interfaces

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:

  • Handlers depend only on what they need
  • Easy to test (mock only relevant methods)
  • Easy to implement (adapter implements one responsibility at a time)
  • Clear separation of concerns

When to Split vs Keep Together

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;
}

Method Design

Guidelines

1. Use Domain Language, Not Technical Language

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
}

2. Return Domain Objects, Not Primitives

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;
}

3. Accept Domain Types, Not Primitives

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.


4. Design for Readability

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?

5. Avoid Leaking Implementation Details

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.


6. Design for Testability

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

Common Port Patterns

Pattern 1: Repository Port (Persistence)

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:

  • One repository per aggregate root
  • Methods use domain language (save, not persist)
  • Return domain entities, not arrays

Pattern 2: Query Port (CQRS Read Side)

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:

  • Separate from write operations (repository)
  • Can return DTOs optimized for display
  • May bypass domain entities for performance

Pattern 3: External Service Port

Purpose: Communicate with external systems (email, payment, etc.).

interface PaymentProcessorInterface
{
    public function charge(PaymentRequest $request): PaymentResult;
    public function refund(RefundRequest $request): RefundResult;
}

Key Points:

  • Express business capability, not technical protocol
  • Accept/return domain objects
  • Hide implementation details (Stripe, PayPal, etc.)

Pattern 4: Event Dispatcher Port

Purpose: Publish domain events.

interface EventDispatcherInterface
{
    public function dispatch(DomainEvent $event): void;
}

Key Points:

  • Generic interface for all events
  • Domain events are first-class citizens
  • Infrastructure handles routing

Pattern 5: Specification Port (Query Builder)

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:

  • Allows complex filtering without polluting repository
  • Composable specifications
  • Advanced pattern, use sparingly

Anti-Patterns to Avoid

Anti-Pattern 1: Generic Repository

AVOID:

interface GenericRepositoryInterface
{
    public function save(object $entity): void;
    public function findById(string $id): ?object;
    public function findAll(): array;
}

Problems:

  • Type-unsafe (object and string are too generic)
  • Loses domain specificity
  • No type hinting benefits

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;
}

Anti-Pattern 2: Repositories with Business Logic

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);
    }
}

Anti-Pattern 3: Query Methods Returning Scalar Arrays

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,
    ) {}
}

Anti-Pattern 4: Ports Depending on Infrastructure

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
}

Real-World Examples

Port/Out Examples (Business Needs)

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....
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky