Skip to content

Port & Adapter Pattern

The Plugin system implements the Port & Adapter pattern (Hexagonal Architecture). This pattern decouples the framework core from external dependencies through clear contracts (Ports) and swappable implementations (Adapters).

Core Concept

Ports are interfaces defining contracts without implementation details. They represent what the framework needs.

Adapters are concrete implementations of Ports. Multiple adapters can implement the same port, providing different technical solutions.

// Port (interface)
interface LoggerInterface
{
    public function info(string $message, array $context = []): void;
}

// Adapters (implementations)
final class FileLogger implements LoggerInterface { /* ... */ }
final class NullLogger implements LoggerInterface { /* ... */ }

Benefits

Flexibility

Replace implementations without changing business logic:

// Switch via DI configuration
LoggerInterface::class => DI\autowire(FileLogger::class),

Testability

Use test-friendly implementations:

// Fast, deterministic tests
ClockInterface::class => DI\autowire(FrozenClock::class),
TransactionInterface::class => DI\autowire(InMemoryTransaction::class),

Isolation

Business logic depends on interfaces, not concrete implementations:

public function __construct(
    private BookRepositoryInterface $bookRepository,  // Port
    private EventBusInterface $eventBus,              // Port
) {}

Available Plugins

See Plugins for the complete list of available plugins with their ports and adapters.

Creating Custom Plugins

  1. Define Port interface in src/Plugin/{Name}/Port/
  2. Implement Adapter in src/Plugin/{Name}/Adapter/
  3. (Optional) Add internal utilities in src/Plugin/{Name}/Internal/
  4. Register binding in config/{app}/container.php
  5. Application code uses only the interface

Port Types

Driving Ports (Primary)

Called by external actors to trigger application behavior:

  • Controllers calling CommandBusInterface
  • API endpoints calling QueryBusInterface
  • CLI commands calling application services

Driven Ports (Secondary)

Called by the application to interact with external systems:

  • BookRepositoryInterface → Database access
  • LoggerInterface → Logging system
  • SessionInterface → Session storage
  • EventBusInterface → Event publishing

Plugin Categories

Plugins fall into two categories based on how they're used:

Self-Contained Plugins

Provide complete, ready-to-use adapters. Applications select the appropriate adapter via DI container configuration.

Plugin Port Adapters
Logger LoggerInterface MonologLogger, NullLogger
Session SessionInterface OdanSession
Transaction TransactionInterface PdoTransaction, InMemoryTransaction
IdGenerator IdGeneratorInterface TimestampIdGenerator, UuidGenerator
Clock ClockInterface SystemClock, FrozenClock
PasswordHasher PasswordHasherInterface BcryptPasswordHasher
Authorization AuthorizationInterface RbacAuthorization
CommandBus CommandBusInterface SyncCommandBus, TransactionalCommandBus
QueryBus QueryBusInterface SyncQueryBus
EventBus EventBusInterface SyncEventBus
SqlDriver SqlDriverInterface SqliteDriver, MysqlDriver, PostgresqlDriver

Extension Plugins

Provide abstract base classes that applications extend per-entity. Concrete adapters live in the application's Infrastructure layer.

Plugin Abstract Base Application Creates
Repository AbstractInMemoryRepository, AbstractSqlRepository InMemoryBookRepository, SqliteBookRepository

Example: Repository Extension

// Application extends the abstract base
final class InMemoryBookRepository extends AbstractInMemoryRepository implements BookRepositoryInterface
{
    // Inherits save(), findById(), delete() behavior
    // Add entity-specific methods
}

Internal/ Directory

Some plugins contain an Internal/ directory with:

  • Abstract base classes for adapter implementation
  • Utility classes supporting the Port/Adapter contract
  • Conventions and helpers

Important: Classes in Internal/ are implementation details. Application code should not import them directly—use only the public Port interfaces.

src/Plugin/Repository/
├── Port/
│   └── RepositoryInterface.php      ← Import this
├── Adapter/
│   └── InMemory/, Sqlite/, ...
└── Internal/
    └── AbstractInMemoryRepository.php  ← Extend, don't import directly

See Also