symfony-ports-adapters
Symfony ports and adapters — port interfaces, adapter implementations, dependency injection, autowiring, repository interfaces. Triggers on: port, adapter, interface, repository interface, DI, autowir
SKILL.mdskills/symfony-ports-adapters/SKILL.md
GitHubSymfony Ports & Adapters
You are an expert in the Ports & Adapters pattern within Symfony hexagonal architecture.
When to Activate
- User needs to define a port (interface) for external dependency
- User needs to implement an adapter
- User asks about DI configuration or autowiring
- User wants to bind an interface to a concrete implementation
Version Notes
| Feature | Requires | Fallback |
|---|---|---|
alias: in services.yaml |
all versions | - |
when@test: / when@dev: blocks |
Symfony 5.3 | services_test.yaml |
#[AsAlias(PortInterface::class)] on the adapter |
Symfony 6.3 | YAML alias |
#[AsAlias(..., when: 'test')] |
Symfony 7.3 | when@test: YAML block |
#[AsDecorator(decorates: ...)] |
Symfony 6.1 | decorates: in YAML |
Repeatable #[AsDecorator] |
Symfony 7.4 | one decorator per class |
Default to YAML aliases: they work on every version and keep all bindings visible in one file.
Port = Interface in Domain
Ports are interfaces that define contracts for external dependencies. They live in Domain/{Module}/Port/ (shared ones in Domain/Shared/Port/).
Rules
- One interface per concern (Single Responsibility)
- Domain-centric naming (not tech-centric):
UserRepositoryInterface, notDoctrineUserRepository - Only domain types in signatures (value objects, entities, read models, scalars)
- No framework types in port signatures (
Request,Collection,UploadedFile,Uuid, ...) - IDs, clocks and password hashing are ports too
Common Port Types
| Port Type | Example |
|---|---|
| Repository (write) | UserRepositoryInterface |
| Read repository | UserReadRepositoryInterface |
| External Service | PaymentGatewayInterface |
| Notification | NotificationServiceInterface |
| File Storage | FileStorageInterface |
| Identity | IdGeneratorInterface |
| Security | PasswordHasherInterface |
Template
namespace App\Domain\{Module}\Port;
use App\Domain\{Module}\Entity\{Entity};
use App\Domain\{Module}\ValueObject\{Entity}Id;
interface {Entity}RepositoryInterface
{
public function save({Entity} $entity): void;
public function findById({Entity}Id $id): ?{Entity};
}
Adapter = Implementation in Infrastructure
Adapters implement ports using specific technologies. They live in Infrastructure/{Module}/.
Rules
- Implements exactly one port interface
- Contains ALL technology-specific code
- Named with technology prefix:
Doctrine{Entity}Repository,Stripe{Payment}Gateway final readonly classwhen possible- Translates technology exceptions into domain exceptions where callers need to react
Template
namespace App\Infrastructure\{Module}\Persistence;
use App\Domain\{Module}\Port\{Entity}RepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
final readonly class Doctrine{Entity}Repository implements {Entity}RepositoryInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
) {
}
// implement all port methods
}
DI Configuration
# config/services.yaml
services:
# Bind ports to adapters
App\Domain\User\Port\UserRepositoryInterface:
alias: App\Infrastructure\User\Persistence\DoctrineUserRepository
App\Domain\Payment\Port\PaymentGatewayInterface:
alias: App\Infrastructure\Payment\ExternalService\StripePaymentGateway
App\Domain\Notification\Port\NotificationServiceInterface:
alias: App\Infrastructure\Notification\ExternalService\SymfonyMailerAdapter
Attribute alternative (Symfony 6.3+), on the adapter:
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
#[AsAlias(UserRepositoryInterface::class)]
final readonly class DoctrineUserRepository implements UserRepositoryInterface { /* ... */ }
References
See references/ for detailed guides:
port-adapter-examples.md- Full examples for various port typesdi-configuration.md- Advanced DI configuration patterns