Appliquer le Domain-Driven Design à PHP et Symfony : Un Guide Pratique

· Silas Joisten · Temps de lecture: 4 minutes
Domain Driven Design practical approach

Le Domain-Driven Design (DDD) s'applique à Symfony grâce à des Value Objects, des dépôts et des contextes bornés. Dans cet article, découvrez les étapes concrètes pour construire des applications PHP évolutives.

Introduction

Le Domain-Driven Design (DDD) aide les développeurs à structurer leurs applications autour de la logique métier concrète tout en gardant une séparation claire des responsabilités. Dans mon précédent article, j’ai expliqué les fondements théoriques du DDD. Aujourd’hui, nous allons plonger dans un exemple concret en construisant un client API météo utilisant OpenWeatherMap et Symfony.

Qu'allez-vous apprendre ?

  • Comment structurer un client API selon les principes du Domain-Driven Design

  • Utiliser des Value Objects pour encapsuler la logique des requêtes et réponses

  • Implémenter un repository pour gérer les interactions avec l'API

  • Séparer la logique API de la logique métier

  • Utiliser des clients HTTP scénarisés dans Symfony pour une meilleure configuration

À la fin de cet article, vous serez en mesure d'intégrer des APIs tierces dans Symfony tout en maintenant une architecture propre.

Cas d’utilisation : récupération de données météo via OpenWeatherMap

Nous allons construire un client API conforme au DDD pour récupérer les données météo en temps réel depuis OpenWeatherMap.

Détails de l’API

URL de base :

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

Paramètres de requête :

  • q (Nom de la ville, par ex. "Paris")

  • units (Unités : "metric", "imperial", "standard")

Exemple de requête :

GET https://api.openweathermap.org/data/2.5/weather?q=Paris&units=metric&appid=YOUR_API_KEY

Exemple de réponse :

{
  "coord": { "lon": 2.3488, "lat": 48.8534 },
  "weather": [{ "main": "Clouds", "description": "few clouds" }],
  "main": { "temp": 18.3, "pressure": 1015, "humidity": 70 },
  "wind": { "speed": 3.5, "deg": 150 },
  "name": "Paris"
}

Étape 1 : Définir l’interface de l’API

Pour découpler la logique de l'API du reste de l'application, nous définissons une interface qui décrit comment doit fonctionner le client API.

// ...
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;
}

Pourquoi utiliser une interface ?

  • Elle abstrait les dépendances externes (par ex. OpenWeatherMap)

  • Elle assure la flexibilité (facile à changer de fournisseur API)

  • Elle permet l’injection de dépendance pour les tests ou les mocks

Notre application reste ainsi faiblement couplée, extensible et testable.

Étape 2 : Implémenter les Value Objects

Region (Value Object)

Elle encapsule et valide les noms de ville.

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

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

Unit (Enum)

Elle représente les unités disponibles avec une sécurité de type stricte.

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

WeatherRequest (Value Object)

Elle représente les options de requête de l’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)

Elle valide et structure les données retournées par l’API.

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
    }
}

Pourquoi utiliser des Value Objects ?

  • Assurer l’intégrité des données

  • Encapsuler la logique métier

  • Prévenir les données invalides

  • Faciliter les tests et la lecture du code

Les enums comme Unit améliorent aussi l’expérience développeur en imposant des valeurs valides.

Étape 3 : Implémenter le client API météo

Voici une implémentation du client avec Symfony HttpClient :

// ...
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());
    }
}

Une alternative : le client HTTP scénarisé

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)%'

Modifiez le client comme suit :

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());
    }
}

Étape 4 : Organisation du répertoire

En Domain-Driven Design, une structure claire est essentielle. Nous utilisons le Bridge Pattern pour isoler la logique externe de l’API.

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

Pourquoi cette structure ?

  • Isoler l’intégration API sous Bridge\WeatherApi

  • Garder la logique métier indépendante de l’API

  • Faciliter la maintenance et les tests

Il est possible de changer de fournisseur d’API sans impacter la logique métier.

Conclusion

Cet exemple montre comment intégrer une API externe dans Symfony avec le Domain-Driven Design. En résumé, il faut :

  • Encapsuler la logique API dans un repository

  • Utiliser des Value Objects pour les requêtes/réponses

  • Masquer l’intégration API derrière une interface

  • (Optionnel) Utiliser des clients HTTP scénarisés Symfony

Avec cette architecture, votre client API Symfony sera maintenable, testable et découplé.

Prêts à appliquer le Domain-Driven Design à vos projets Symfony ?

Les experts Symfony et PHP de SensioLabs vous aident à créer un projet efficace en matière de structure et avec une analyse basée sur le domaine.

Cela pourrait aussi vous intéresser

Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025 : Notre bilan et les moments forts

Après une première édition emblématique en 2019, SymfonyCon a fait son grand retour à Amsterdam. Dès les premières minutes, on sentait l’énergie d’un rendez-vous très attendu : plus de 1 200 participants, 39 nationalités, les retrouvailles avec la communauté, de belles découvertes… et une ambiance de folie. Cette année, l’événement avait une saveur toute particulière puisqu’il s’agissait de l’édition spéciale anniversaire des 20 ans de Symfony. SensioLabs y était : on vous raconte tout !

En savoir plus
PHP 8.5 URI extension
Oskar Stark

La nouvelle extension URI de PHP 8.5 : Une révolution pour l'analyse des URL

PHP 8.5 introduit une nouvelle extension URI puissante qui modernise la gestion des URL. Grâce au support des standards RFC 3986 et WHATWG, la nouvelle classe Uri fournit des objets immuables, des interfaces fluides et une validation appropriée, résolvant ainsi toutes les limites de la fonction historique parse_url(). Cet articl présente des exemples pratiques avant/après et explique quand utiliser chaque standard.

En savoir plus
Open in new tab
Silas Joisten

Le piège des onglets: pourquoi forcer l'ouverture de nouveaux onglets est une mauvaise pratique en UX

Nous l'avons tous fait — ajouter target="_blank" à un lien pour « aider les utilisateurs » à rester sur notre site. Mais ce qui semble être une commodité inoffensive crée souvent de la confusion, diminue l'accessibilité et introduit des risques de sécurité cachés.

En savoir plus
3 dog heads
Mathieu Santostefano

Venez avec votre propre client HTTP

Libérez-vous des dépendances rigides de vos SDK PHP. Dans cet article, apprenez à utiliser les normes PSR-7, PSR-17 et PSR-18, ainsi que la bibliothèque php-http/discovery, pour permettre à vos utilisateurs d'utiliser le client HTTP de leur choix, qu'il s'agisse de Guzzle, de Symfony HttpClient ou d'un autre. Un incontournable pour les développeurs PHP et Symfony.

En savoir plus
Blue sign on a building with several Now What? letters
Thibaut Chieux

Comment prioriser les messages lors du développement d'applications asynchrones avec Symfony Messenger

Le traitement asynchrone offre des avantages tels que la découplage des processus et des temps de réponse plus rapides, mais la gestion des priorités des messages peut s'avérer complexe. Pour traiter des tâches allant de la réinitialisation de mot de passe à des exports complexes, il est essentiel de garantir la livraison rapide des messages critiques. Cet article examine les problèmes fréquents liés au traitement asynchrone et propose des solutions avec Symfony Messenger pour optimiser votre application sans refonte majeure.

En savoir plus
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

Les Lazy Services de Symfony : Boostez votre DX en utilisant les Service Subscribers

Optimisez la performance de votre application Symfony et l'expérience développeur ! Apprenez à utiliser les Service Subscribers et les attributs de chargement différé des services afin de réduire l'instanciation rapide, de simplifier les dépendances et de créer un code modulaire et maintenable.

En savoir plus
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

Les verbes HTTP : votre Guide Complet

Apprenez les bases des verbes HTTP : GET, POST, PUT, DELETE, et plus encore. Cet article vous explique leur fonctionnement, leurs utilisations et les conséquences en matière de sécurité.

En savoir plus
Toy factory production line
Silas Joisten

Boostez vos tests Symfony avec Zenstruck Foundry

Zenstruck Foundry a révolutionné notre manière d’écrire des tests dans Symfony. Dans cet article, vous apprendrez comment des fabriques expressives, des données de test isolées et une expérience développeur plus fluide nous ont permis d’optimiser nos flux de tests et d’améliorer la productivité.

En savoir plus
Image