Anwendung von Domain-Driven Design in PHP und Symfony: Ein praktischer Leitfaden

· Silas Joisten · 4 Minuten zum Lesen
Domain Driven Design practical approach

Erfahre anhand praktischer Beispiele, wie du die Prinzipien des Domain-Driven Design (DDD) in Symfony anwendest. Entdecke die Leistungsfähigkeit von Value Objects, Repositories und Bounded Contexts.

Einführung

Domain-Driven Design (DDD) hilft Entwickler:innen dabei, ihre Anwendungen rund um echte Geschäftslogik zu strukturieren und dabei die Zuständigkeiten klar zu trennen. In meinem vorherigen Beitrag habe ich die theoretischen Grundlagen von DDD erklärt, aber heute tauchen wir in ein praxisnahes Beispiel ein: Wir bauen einen Wetter-API-Client mit OpenWeatherMap und Symfony.

Was wirst du lernen?

  • Wie man einen API-Client nach DDD-Prinzipien strukturiert

  • Wie man Value Objects nutzt, um Anfrage- und Antwortlogik zu kapseln

  • Wie man ein Repository implementiert, um API-Interaktionen zu steuern

  • Wie man API-Logik von der Geschäftslogik trennt

  • Wie man scoped HTTP-Clients in Symfony verwendet – für bessere Konfiguration

Am Ende dieses Beitrags wirst du in der Lage sein, Drittanbieter-APIs in Symfony zu integrieren, ohne deine Architektur zu gefährden.

Use Case: Wetterdaten von OpenWeatherMap abrufen

Wir bauen einen DDD-konformen API-Client, der aktuelle Wetterdaten von OpenWeatherMap abruft.

API-Details

  • Basis-URL:

    https://api.openweathermap.org/data/2.5/weather
  • Anfrageparameter:

    • q (Stadtname, z. B. "Berlin")

    • units (Maßeinheit: "metric", "imperial", "standard")

  • Beispiel-Anfrage:

    GET https://api.openweathermap.org/data/2.5/weather?q=Berlin&units=metric&appid=YOUR_API_KEY
  • Beispiel-Antwort:

    {
      "coord": { "lon": 13.4105, "lat": 52.5244 },
      "weather": [{ "main": "Clouds", "description": "scattered clouds" }],
      "main": { "temp": 21.5, "pressure": 1012, "humidity": 60 },
      "wind": { "speed": 5.1, "deg": 220 },
      "name": "Berlin"
    }

Schritt 1: API-Interface definieren

Um unsere API-Logik vom Rest der Anwendung zu entkoppeln, definieren wir ein Interface, das beschreibt, wie der API-Client funktionieren soll.

// ...
namespace App\Weather\Api;

use App\Weather\Domain\WeatherRequest;
use App\Weather\Domain\WeatherResponse;
use App\Weather\Domain\Region;

interface WeatherApiInterface
{
    public function forRegion(Region $region, WeatherRequest $request = new WeatherRequest()): WeatherResponse;
}

Warum ein Interface verwenden?

  • Abstrahiert externe Abhängigkeiten (z. B. OpenWeatherMap)

  • Sichert Flexibilität (einfacher API-Anbieter-Wechsel)

  • Ermöglicht Dependency Injection für Tests und Mocks

Durch diese Trennung bleibt unsere Anwendung lose gekoppelt und lässt sich einfach erweitern und testen.

Schritt 2: Value Objects implementieren

Region (Value Object)

Kapselt und validiert Städtenamen.

// ...
use Webmozart\Assert\Assert;

final readonly class Region
{
    public function __construct(public string $value)
    {
        Assert::stringNotEmpty($value);
        Assert::notWhitespaceOnly($value);
    }
}

Unit (Enum Value Object)

Repräsentiert vordefinierte Maßeinheiten und sorgt für strikte Typsicherheit.

enum Unit: string
{
    case METRIC = 'metric';
    case IMPERIAL = 'imperial';
    case STANDARD = 'standard';
}

WeatherRequest (Value Object)

Repräsentiert alle möglichen Filter oder Abfrageoptionen der API.

// ...
final readonly class WeatherRequest implements \JsonSerializable
{
    public function __construct(
        public Unit $unit = Unit::METRIC,
    ) {
    }

    public function jsonSerialize(): array
    {
        return [
            'unit' => $this->unit->value,
        ];
    }
}

WeatherResponse (Value Object)

Validiert und strukturiert die Wetterdaten der API-Antwort.

use Webmozart\Assert\Assert;

final readonly class WeatherResponse
{
    public function __construct(array $values)
    {
        Assert::keyExists($values, 'name');
        $this->city = $values['name'];

        Assert::keyExists($values, 'main');
        Assert::keyExists($values['main'], 'temp');
        $this->temperature = $values['main']['temp'];

        Assert::keyExists($values, 'weather');
        Assert::isList($values['weather']);
        Assert::count($values['weather'], 1);
        Assert::keyExists($values['weather'][0], 'description');
        $this->description = $values['weather'][0]['description'];
        
        // of course, this is a very simplified example
    }
}

Warum Value Objects?

  • Stellen Datenintegrität sicher

  • Kapseln Logik innerhalb der Domäne

  • Verhindern ungültige Daten

  • Erleichtern das Testen und das Verständnis

Die Verwendung von Enums wie Unit verbessert zusätzlich die Developer Experience (DX) durch klare Einschränkungen und bessere Autovervollständigung.

Schritt 3: Den Weather API Client implementieren

Nun erstellen wir eine Symfony HttpClient-basierte Implementierung unseres API-Clients.

// ...
final class WeatherApi implements WeatherApiInterface
{
    public function __construct(
        private HttpClientInterface $client,
        #[\SensitiveParameter]
        #[Autowire(env: 'OPENWEATHER_API_KEY')]
        private string $apiKey
    ) {}

    public function forRegion(Region $region, WeatherRequest $request = new WeatherRequest()): WeatherResponse
    {
        $response = $this->client->request('GET', 'https://api.openweathermap.org/data/2.5/weather', [
            'query' => [...$request->jsonSerialize(), 'q' => $region->name, 'appid' => $this->apiKey],
        ]);

        return new WeatherResponse($response->toArray());
    }
}

Alternative: Scoped HTTP Client nutzen

Symfony erlaubt es, einen spezifisch konfigurierten HTTP-Client für OpenWeatherMap zu definieren:

framework:
  http_client:
    scoped_clients:
      openweather.client:
        base_uri: 'https://api.openweathermap.org/data/2.5/weather'
        headers:
          Accept: 'application/json'
        query:
          appid: '%env(OPENWEATHER_API_KEY)%'

Und nun passen wir unseren Client entsprechend an:

final class WeatherApi implements WeatherApiInterface
{
    public function __construct(
+        private HttpClientInterface $openweatherClient,
-        private HttpClientInterface $client,
-        #[\SensitiveParameter]
-        #[Autowire(env: 'OPENWEATHER_API_KEY')]
-        private string $apiKey
    ) {}

    public function forRegion(Region $region, WeatherRequest $request = new WeatherRequest()): WeatherResponse
    {
-        $response = $this->client->request('GET', 'https://api.openweathermap.org/data/2.5/weather', [
-            'query' => [...$request->jsonSerialize(), 'q' => $region->name, 'appid' => $this->apiKey],
+        $response = $this->openweatherClient->request('GET', 'https://api.openweathermap.org/data/2.5/weather', [
+            'query' => [...$request->jsonSerialize(), 'q' => $region->name],

        ]);

        return new WeatherResponse($response->toArray());
    }
}

Schritt 4: Verzeichnisstruktur verstehen

Bei der Arbeit mit DDD ist eine klare Strukturierung entscheidend. Wir nutzen das Bridge-Pattern, um externe API-Logik von der Domäne zu trennen.

src/
├── Bridge/
│   ├── WeatherApi/
│   │   ├── Domain/
│   │   │   ├── Region.php
│   │   │   ├── Unit.php
│   │   │   ├── WeatherRequest.php
│   │   │   ├── WeatherResponse.php
│   │   ├── WeatherApi.php
│   │   ├── WeatherApiInterface.php

Warum diese Struktur?

  • Kapselt die API-Integration unter Bridge\WeatherApi

  • Hält Domänenlogik unabhängig von API-Details

  • Erleichtert Wartung und Tests, da externe Abhängigkeiten isoliert sind

Durch diesen Aufbau kann der API-Anbieter oder die konkrete Implementierung ausgetauscht werden, ohne Auswirkungen auf die Kernlogik der Anwendung.

Fazit

Dieses Beispiel zeigt, wie man eine externe API in Symfony nach DDD-Prinzipien integriert:

  • API-Logik in ein Repository kapseln

  • Value Objects zur Validierung von Anfragen und Antworten verwenden

  • API-Interaktionen hinter einem Interface abstrahieren

  • (Optional) Scoped HTTP Clients für saubere Konfiguration verwenden

Wenn du deinen Symfony-API-Client auf diese Weise strukturierst, erreichst du eine hohe Wartbarkeit, Entkopplung und Testbarkeit.

Möchtest du DDD in deinen Symfony-Projekten anwenden?

Bei SensioLabs können unsere Symfony- und PHP-Experten Ihnen helfen, Ihre Projekte effizient zu strukturieren: intelligentes Code-Factoring, gute Entwicklungsmethoden und skalierbare Architektur.

Das könnte dich auch interessieren

Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025: Unser Rückblick und die Highlights

Nach einer legendären ersten Ausgabe im Jahr 2019 feierte die SymfonyCon ihr großes Comeback in Amsterdam. Von Anfang an war die Energie einer mit Spannung erwarteten Konferenz zu spüren: mehr als 1.200 Teilnehmer, 39 Nationalitäten, das größte Treffen der Symfony-Community des Jahres, großartige Entdeckungen ... und eine ausgelassene Atmosphäre. Dieses Jahr war etwas ganz Besonderes, denn es war das 20-jährige Jubiläum von Symfony. SensioLabs war dabei: Wir berichten Ihnen ausführlich über unsere Erfahrungen dort!

Mehr erfahren
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's neue URI-Erweiterung: Ein Game-Changer für URL-Parsing

PHP 8.5 führt eine leistungsstarke neue URI-Erweiterung ein, die die URL-Verarbeitung modernisiert. Mit Unterstützung für RFC 3986 und WHATWG-Standards bietet die neue Uri-Klasse unveränderliche Objekte, fluent Interfaces und korrekte Validierung - und behebt alle Einschränkungen der veralteten parse_url()-Funktion. Dieser Leitfaden zeigt praktische Vorher/Nachher-Beispiele und erklärt, wann welcher Standard zu verwenden ist.

Mehr erfahren
Open in new tab
Silas Joisten

Die Tab-Falle: Warum das Erzwingen neuer Tabs eine schlechte UX ist

Wir haben es alle schon getan — target="_blank" zu einem Link hinzugefügt, um „Benutzern zu helfen", auf unserer Website zu bleiben. Aber was sich wie eine harmlose Bequemlichkeit anfühlt, führt oft zu Verwirrung, beeinträchtigt die Barrierefreiheit und birgt versteckte Sicherheitsrisiken.

Mehr erfahren
3 dog heads
Mathieu Santostefano

Lass die Nutzer des SDK ihren eigenen HTTP-Client nutzen

Befreie dich von starren Abhängigkeiten in deinen PHP-SDKs. Erfahre, wie du die Standards PSR-7, PSR-17 und PSR-18 zusammen mit PHP-HTTP/Discovery nutzt, um deinen Benutzern die Verwendung ihres bevorzugten HTTP-Clients zu ermöglichen – sei es Guzzle, Symfony HttpClient oder ein anderes Tool. Ein Muss für PHP- und Symfony-Entwickler.

Mehr erfahren
Blue sign on a building with several Now What? letters
Thibaut Chieux

Wie man Nachrichten beim Aufbau asynchroner Anwendungen mit dem Symfony-Messenger priorisiert

Die asynchrone Verarbeitung bietet Vorteile wie entkoppelte Prozesse und schnellere Reaktionszeiten. Die Verwaltung von Nachrichtenprioritäten kann jedoch zu einer Herausforderung werden. Bei Aufgaben, die vom Zurücksetzen von Passwörtern bis hin zu komplexen Exporten reichen, ist die rechtzeitige Zustellung kritischer Nachrichten unerlässlich. Dieser Artikel befasst sich mit häufigen Problemen bei der asynchronen Verarbeitung und zeigt Lösungen mit Symfony Messenger auf, mit denen Sie Ihre Anwendung ohne umfangreiches Refactoring optimieren können.

Mehr erfahren
Two images: on the left many cars stuck in a traffic jam with the sign "All directions" above, on the right a blue car moving forward alone on the highway with the sign "Service Subscriber" and a Symfony logo above
Steven Renaux

Symfony Lazy Services mit Stil – Steigere deine Entwicklererfahrung mit Service Subscribers

Steigere die Performance und Developer Experience (DX) deiner Symfony-App! Erfahre, wie du Service Subscribers und Traits für das verzögerte Laden von Services verwendest, um die sofortige Instanziierung zu reduzieren, Abhängigkeiten zu vereinfachen und modularen, wartbaren Code zu schreiben.

Mehr erfahren
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

HTTP-Verben: Der ultimative Leitfaden

Hier erklären wir dir die Grundlagen von GET, POST, PUT, DELETE und mehr. In diesem Artikel erfährst du alles über die Funktionsweise, die Anwendungsmöglichkeiten und die Sicherheitsauswirkungen.

Mehr erfahren
Toy factory production line
Silas Joisten

Symfony-Tests mit Zenstruck Foundry auf das nächste Level bringen

Zenstruck Foundry hat die Art und Weise, wie wir Tests in Symfony schreiben, revolutioniert. In diesem Beitrag erfährst du, wie uns ausdrucksstarke Factories, isolierte Testdaten und ein reibungsloseres Entwicklererlebnis dabei geholfen haben, unseren Test-Workflow zu optimieren und die Produktivität zu steigern.

Mehr erfahren
Image