Hexagonal Architecture (Ports and Adapters) | Enjyn Gruppe
Hallo Welt
Hallo Welt
Original Lingva Deutsch
Übersetzung wird vorbereitet...
Dieser Vorgang kann bis zu 60 Sekunden dauern.
Diese Seite wird erstmalig übersetzt und dann für alle Besucher gespeichert.
0%
DE Zurück zu Deutsch
Übersetzung durch Lingva Translate

235 Dokumentationen verfügbar

Wissensdatenbank

Hexagonal Architecture Ports Adapters

Zuletzt aktualisiert: 20.01.2026 um 11:26 Uhr

Hexagonal Architecture (Ports and Adapters)

Hexagonal Architecture isoliert die Business-Logik von externen Systemen. Lernen Sie das Ports and Adapters Pattern für flexible und testbare Software.

Das Konzept

┌─────────────────────────────────────────────────────────────┐
│                 HEXAGONAL ARCHITECTURE                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Auch bekannt als: Ports and Adapters, Onion Architecture │
│                                                             │
│                        ┌───────────┐                        │
│                    REST│  Adapter  │CLI                     │
│                        └─────┬─────┘                        │
│                              │                              │
│                        ┌─────▼─────┐                        │
│                        │   PORT    │ (Input)                │
│                        │(Interface)│                        │
│                        └─────┬─────┘                        │
│                              │                              │
│         ┌────────────────────┼────────────────────┐        │
│         │                    │                     │        │
│         │            ┌───────▼───────┐            │        │
│         │            │               │            │        │
│         │            │    DOMAIN     │            │        │
│         │            │   (Business   │            │        │
│         │            │    Logic)     │            │        │
│         │            │               │            │        │
│         │            └───────┬───────┘            │        │
│         │                    │                     │        │
│         └────────────────────┼────────────────────┘        │
│                              │                              │
│                        ┌─────▼─────┐                        │
│                        │   PORT    │ (Output)               │
│                        │(Interface)│                        │
│                        └─────┬─────┘                        │
│                              │                              │
│                        ┌─────▼─────┐                        │
│                   MySQL│  Adapter  │Redis                   │
│                        └───────────┘                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Komponenten

Komponente Beschreibung Beispiele
Domain Reine Geschäftslogik, keine Abhängigkeiten Entities, Value Objects, Services
Ports Interfaces zur Außenwelt UserRepository, PaymentGateway
Adapters Implementierungen der Ports MySQLUserRepository, StripeGateway

Projektstruktur

src/
├── Domain/                    # Kern - keine Abhängigkeiten
│   ├── Model/
│   │   ├── User.php
│   │   ├── Order.php
│   │   └── ValueObject/
│   │       ├── Email.php
│   │       └── Money.php
│   ├── Service/
│   │   ├── OrderService.php
│   │   └── PricingService.php
│   ├── Repository/            # Port Interfaces
│   │   ├── UserRepositoryInterface.php
│   │   └── OrderRepositoryInterface.php
│   └── Event/
│       └── OrderPlaced.php
│
├── Application/               # Use Cases / Application Services
│   ├── UseCase/
│   │   ├── PlaceOrder/
│   │   │   ├── PlaceOrderCommand.php
│   │   │   └── PlaceOrderHandler.php
│   │   └── GetUser/
│   │       ├── GetUserQuery.php
│   │       └── GetUserHandler.php
│   └── Port/                  # Secondary Ports
│       ├── PaymentGatewayInterface.php
│       └── NotificationServiceInterface.php
│
├── Infrastructure/            # Adapter Implementations
│   ├── Persistence/
│   │   ├── Doctrine/
│   │   │   └── DoctrineUserRepository.php
│   │   └── InMemory/
│   │       └── InMemoryUserRepository.php
│   ├── Payment/
│   │   ├── StripePaymentGateway.php
│   │   └── PayPalPaymentGateway.php
│   └── Notification/
│       └── EmailNotificationService.php
│
└── Adapter/                   # Primary Adapters (Input)
    ├── Http/
    │   ├── Controller/
    │   │   └── OrderController.php
    │   └── Request/
    │       └── PlaceOrderRequest.php
    ├── Cli/
    │   └── ProcessOrdersCommand.php
    └── GraphQL/
        └── OrderResolver.php

Domain Layer

// Domain/Model/Order.php
// Reine Business-Logik, keine Framework-Abhängigkeiten

namespace Domain\Model;

class Order
{
    private OrderId $id;
    private UserId $userId;
    private array $items = [];
    private OrderStatus $status;
    private Money $total;

    public function __construct(OrderId $id, UserId $userId)
    {
        $this->id = $id;
        $this->userId = $userId;
        $this->status = OrderStatus::PENDING;
        $this->total = Money::zero('EUR');
    }

    public function addItem(Product $product, int $quantity): void
    {
        if ($quantity <= 0) {
            throw new InvalidQuantityException('Quantity must be positive');
        }

        if (!$product->isAvailable()) {
            throw new ProductNotAvailableException($product->id());
        }

        $this->items[] = new OrderItem($product, $quantity);
        $this->recalculateTotal();
    }

    public function confirm(): void
    {
        if ($this->status !== OrderStatus::PENDING) {
            throw new InvalidOrderStateException('Can only confirm pending orders');
        }

        if (empty($this->items)) {
            throw new EmptyOrderException('Cannot confirm empty order');
        }

        $this->status = OrderStatus::CONFIRMED;
    }

    private function recalculateTotal(): void
    {
        $this->total = Money::zero('EUR');
        foreach ($this->items as $item) {
            $this->total = $this->total->add($item->lineTotal());
        }
    }
}
// Domain/Repository/OrderRepositoryInterface.php
// Port Interface - definiert WAS, nicht WIE

namespace Domain\Repository;

interface OrderRepositoryInterface
{
    public function save(Order $order): void;
    public function findById(OrderId $id): ?Order;
    public function findByUser(UserId $userId): array;
    public function nextId(): OrderId;
}

Application Layer (Use Cases)

// Application/UseCase/PlaceOrder/PlaceOrderCommand.php

namespace Application\UseCase\PlaceOrder;

class PlaceOrderCommand
{
    public function __construct(
        public readonly string $userId,
        public readonly array $items,  // [{productId, quantity}]
    ) {}
}
// Application/UseCase/PlaceOrder/PlaceOrderHandler.php

namespace Application\UseCase\PlaceOrder;

use Domain\Repository\OrderRepositoryInterface;
use Domain\Repository\ProductRepositoryInterface;
use Application\Port\PaymentGatewayInterface;

class PlaceOrderHandler
{
    public function __construct(
        private OrderRepositoryInterface $orderRepository,
        private ProductRepositoryInterface $productRepository,
        private PaymentGatewayInterface $paymentGateway,
    ) {}

    public function handle(PlaceOrderCommand $command): OrderId
    {
        // 1. Order erstellen (Domain)
        $order = new Order(
            $this->orderRepository->nextId(),
            new UserId($command->userId)
        );

        // 2. Items hinzufügen
        foreach ($command->items as $item) {
            $product = $this->productRepository->findById(
                new ProductId($item['productId'])
            );

            if (!$product) {
                throw new ProductNotFoundException($item['productId']);
            }

            $order->addItem($product, $item['quantity']);
        }

        // 3. Order bestätigen (Domain-Regel)
        $order->confirm();

        // 4. Zahlung verarbeiten (via Port)
        $this->paymentGateway->charge(
            $order->userId(),
            $order->total()
        );

        // 5. Persistieren (via Port)
        $this->orderRepository->save($order);

        return $order->id();
    }
}

Infrastructure Layer (Adapters)

// Infrastructure/Persistence/Doctrine/DoctrineOrderRepository.php

namespace Infrastructure\Persistence\Doctrine;

use Domain\Repository\OrderRepositoryInterface;
use Domain\Model\Order;
use Doctrine\ORM\EntityManagerInterface;

class DoctrineOrderRepository implements OrderRepositoryInterface
{
    public function __construct(
        private EntityManagerInterface $em
    ) {}

    public function save(Order $order): void
    {
        $this->em->persist($order);
        $this->em->flush();
    }

    public function findById(OrderId $id): ?Order
    {
        return $this->em->find(Order::class, $id->value());
    }

    public function findByUser(UserId $userId): array
    {
        return $this->em->createQueryBuilder()
            ->select('o')
            ->from(Order::class, 'o')
            ->where('o.userId = :userId')
            ->setParameter('userId', $userId->value())
            ->getQuery()
            ->getResult();
    }

    public function nextId(): OrderId
    {
        return new OrderId(Uuid::uuid4()->toString());
    }
}
// Infrastructure/Persistence/InMemory/InMemoryOrderRepository.php
// Für Tests

namespace Infrastructure\Persistence\InMemory;

use Domain\Repository\OrderRepositoryInterface;
use Domain\Model\Order;

class InMemoryOrderRepository implements OrderRepositoryInterface
{
    private array $orders = [];
    private int $nextId = 1;

    public function save(Order $order): void
    {
        $this->orders[$order->id()->value()] = $order;
    }

    public function findById(OrderId $id): ?Order
    {
        return $this->orders[$id->value()] ?? null;
    }

    public function findByUser(UserId $userId): array
    {
        return array_filter(
            $this->orders,
            fn(Order $o) => $o->userId()->equals($userId)
        );
    }

    public function nextId(): OrderId
    {
        return new OrderId((string) $this->nextId++);
    }
}

Adapter Layer (HTTP)

// Adapter/Http/Controller/OrderController.php

namespace Adapter\Http\Controller;

use Application\UseCase\PlaceOrder\PlaceOrderCommand;
use Application\UseCase\PlaceOrder\PlaceOrderHandler;

class OrderController
{
    public function __construct(
        private PlaceOrderHandler $placeOrderHandler
    ) {}

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'user_id' => 'required|string',
            'items' => 'required|array',
            'items.*.product_id' => 'required|string',
            'items.*.quantity' => 'required|integer|min:1',
        ]);

        $command = new PlaceOrderCommand(
            userId: $validated['user_id'],
            items: $validated['items']
        );

        try {
            $orderId = $this->placeOrderHandler->handle($command);

            return response()->json([
                'order_id' => $orderId->value()
            ], 201);

        } catch (ProductNotFoundException $e) {
            return response()->json([
                'error' => "Product not found: {$e->productId()}"
            ], 404);

        } catch (DomainException $e) {
            return response()->json([
                'error' => $e->getMessage()
            ], 400);
        }
    }
}

Dependency Injection

// config/services.php oder DI Container

// Domain Repositories → Infrastructure Implementation
$container->bind(
    OrderRepositoryInterface::class,
    DoctrineOrderRepository::class
);

$container->bind(
    ProductRepositoryInterface::class,
    DoctrineProductRepository::class
);

// Application Ports → Infrastructure Adapters
$container->bind(
    PaymentGatewayInterface::class,
    StripePaymentGateway::class
);

// Für Tests: In-Memory Implementierungen
// $container->bind(OrderRepositoryInterface::class, InMemoryOrderRepository::class);

Vorteile

1. TESTBARKEIT
   ┌────────────────────────────────────────────────────────┐
   │ Domain isoliert testen ohne DB, HTTP, etc.            │
   │ Use Cases mit In-Memory Repositories testen           │
   │ Keine Mocks für externe Services nötig               │
   └────────────────────────────────────────────────────────┘

2. AUSTAUSCHBARKEIT
   ┌────────────────────────────────────────────────────────┐
   │ MySQL → PostgreSQL: Nur Adapter tauschen              │
   │ Stripe → PayPal: Neuer Adapter, gleicher Port         │
   │ REST → GraphQL: Neuer Adapter, gleiche Use Cases      │
   └────────────────────────────────────────────────────────┘

3. UNABHÄNGIGKEIT
   ┌────────────────────────────────────────────────────────┐
   │ Domain kennt keine Frameworks                          │
   │ Business-Logik bleibt bei Framework-Wechsel           │
   │ Technologie-Entscheidungen verschiebbar               │
   └────────────────────────────────────────────────────────┘

4. FOKUS
   ┌────────────────────────────────────────────────────────┐
   │ Domain fokussiert auf Business                         │
   │ Infrastructure fokussiert auf Technik                 │
   │ Klare Trennung der Verantwortungen                    │
   └────────────────────────────────────────────────────────┘
💡 Best Practices: 1. Domain darf KEINE externen Dependencies haben
2. Ports sind Interfaces im Domain/Application Layer
3. Adapters implementieren Ports in Infrastructure
4. Dependency Inversion: High-Level → Interface ← Low-Level
5. Für kleine Projekte oft overkill - pragmatisch bleiben

Weitere Informationen

Enjix Beta

Enjyn AI Agent

Hallo 👋 Ich bin Enjix — wie kann ich dir helfen?
120