Am 3. und 4. April findet mit der SymfonyLive Berlin 2025 die größte Symfony-Konferenz der deutschen Community statt 🥨 Jetzt Ticket buchen


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.

Image