Service HTTP qui interroge OpenWeatherMap pour retourner la météo courante d'une ville. Trois modes d'entrée mutuellement exclusifs : nom de ville, identifiant OpenWeatherMap, ou coordonnées géographiques.
| Stack | PHP 8.5 · Symfony 8.0 · FrankenPHP · Docker |
| Architecture | Monolithe modulaire · DDD / Hexagonal · CQRS read-side |
| Qualité | PHPStan level 8 · PHP-CS-Fixer (PSR-12 + Symfony) · Rector · pyramide PHPUnit (62 tests / 107 assertions) |
| Doc API | OpenAPI 3 + Swagger UI sur http://localhost:8000/api/doc |
Pré-requis : Docker + Docker Compose.
# 1. Démarrer la stack (FrankenPHP sur :8000)
make up
# 2. Installer les dépendances PHP dans le conteneur
make update
# 3. Renseigner votre clé OpenWeatherMap dans un fichier non versionné
echo 'OPEN_WEATHER_MAP_API_KEY=votre_cle_ici' > .env.local
# 4. Vider le cache pour prendre en compte la nouvelle clé
make cache-clearC'est prêt :
- API →
http://localhost:8000/api/weather/current?city=Paris - Swagger UI →
http://localhost:8000/api/doc - OpenAPI JSON →
http://localhost:8000/api/doc.json
Pour arrêter la stack : make down.
L'endpoint GET /api/weather/current accepte exactement un des trois
modes d'interrogation :
# Par nom de ville (avec hint pays optionnel)
curl 'http://localhost:8000/api/weather/current?city=Paris&country=fr'
# Par identifiant OpenWeatherMap
curl 'http://localhost:8000/api/weather/current?id=2988507'
# Par coordonnées géographiques
curl 'http://localhost:8000/api/weather/current?lat=48.8566&lon=2.3522'Réponse 200 (extrait) :
{
"location": { "name": "Paris", "country": "FR", "id": 2988507, "latitude": 48.85, "longitude": 2.35 },
"temperature":{ "value": 17.24, "unit": "celsius" },
"feels_like": { "value": 16.38, "unit": "celsius" },
"humidity": 52,
"pressure": 1015,
"wind": { "speed_m_s": 5.66, "direction_deg": 20 },
"condition": { "main": "Clear", "description": "ciel dégagé", "icon": "01n" },
"observed_at":"2026-04-28T21:58:11+00:00"
}Erreurs (RFC 7807 — application/problem+json) :
| Code | Cause |
|---|---|
| 400 | Aucun critère, critères ambigus, valeur hors plage, type invalide |
| 404 | OpenWeatherMap n'a aucune observation pour cette localisation |
| 500 | Clé API invalide / non autorisée |
| 502 | OpenWeatherMap injoignable ou réponse malformée |
Monolithe modulaire organisé en bounded contexts indépendants. Chaque module suit la même topologie hexagonale (Domain ← Application ← Infrastructure).
src/
├── Shared/ # Kernel partagé (cross-cutting)
│ ├── Domain/Exception/ # DomainException + interfaces marqueurs
│ │ # (BadInputProblem, NotFoundProblem, …)
│ └── Infrastructure/Http/EventListener/ # JsonExceptionListener (RFC 7807)
└── Weather/ # Bounded context "Weather"
├── Domain/ # Pur PHP, zéro dépendance Symfony
│ ├── ValueObject/ # CityName, Coordinates, Temperature, …
│ ├── Query/ # WeatherQuery + 3 impls (sealed-like)
│ ├── Model/Weather # Agrégat read-only
│ ├── Port/WeatherProvider # Interface (port sortant)
│ └── Exception/
├── Application/ # Use-cases (CQRS read-side)
│ ├── Query/GetCurrentWeather + Handler
│ ├── Factory/WeatherQueryFactory # Parsing GET → WeatherQuery
│ └── DTO/CurrentWeatherView # Read-model
└── Infrastructure/ # Adaptateurs
├── Http/Controller/ # GetCurrentWeatherController
└── Provider/OpenWeatherMap/ # Adapter du port WeatherProvider
├── OpenWeatherMapProvider
└── Mapper/ # Payload OWM → Weather aggregate
- DIP — Le domaine ne connaît que
WeatherProvider. L'alias DI route vers l'adaptateur OpenWeatherMap ; en remplacer un autre ne touche que la config. - VOs always-valid —
CityName,Coordinates,Humidity, etc. valident leurs invariants en constructeur. Aucun objet incohérent ne circule. - WeatherQuery sealed-like — interface marqueur + 3
final readonlyclasses ; ajouter un mode d'entrée n'impacte que les adaptateurs concernés. - SRP — Controller (HTTP), Factory (parsing), Handler (use-case), Mapper (sérialisation), Listener (erreur → HTTP) : un seul motif de changement par classe.
- Erreurs typées — chaque exception domaine implémente une interface
marqueur (
BadInputProblem,NotFoundProblem,UpstreamUnavailableProblem,UpstreamConfigurationProblem). LeJsonExceptionListenerles mappe en statuts HTTP sans connaître les modules.
┌──────────────┐
│ Functional │ 6 — endpoint complet via WebTestCase, port stubbé
├──────────────┤
│ Integration │ 8 — provider OWM via Symfony MockHttpClient
├──────────────┤
│ Unit │ 47 — VOs, Factory, Handler, Mapper
└──────────────┘
make test # tout
make test-unit # uniquement la base de la pyramide
make test-integration # uniquement le middle tier
make test-functional # uniquement le sommet (boot du Kernel)make qa # phpstan + cs + rector + tests
make stan # PHPStan level 8 (src + tests)
make cs / cs-fix # PHP-CS-Fixer (PSR-12 + Symfony + PHP84)
make rector # Rector (PHP 8.4 + 7 sets)Variables d'environnement (.env pour les valeurs par défaut, .env.local
pour les secrets, jamais versionné) :
| Variable | Défaut | Description |
|---|---|---|
OPEN_WEATHER_MAP_BASE_URI |
https://api.openweathermap.org |
URL de base d'OpenWeatherMap |
OPEN_WEATHER_MAP_API_KEY |
changeme |
Clé API (à overrider dans .env.local) |
OPEN_WEATHER_MAP_LANG |
fr |
Langue des descriptions |
OPEN_WEATHER_MAP_UNITS |
metric |
metric (°C / m·s⁻¹), imperial, ou défaut Kelvin |
OPEN_WEATHER_MAP_TIMEOUT |
5 |
Timeout HTTP en secondes |
make help # lister toutes les cibles disponibles
make up / down # cycle de vie de la stack Docker
make sh # shell dans le conteneur app
make logs # tail des logs FrankenPHP
make console c="…" # exécuter une commande bin/console