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

234 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