Chapitre 13.1 — HttpClient : consommer des API externes
Partie 13 : Interopérabilité — Chapitre 1/5 Version de référence : Symfony 7.4 (composant HttpClient).
⏱️ TL;DR
- HttpClient est le client HTTP de Symfony :
HttpClientInterface::request($method, $url, $options). Options utiles :query,json,headers,auth_bearer,auth_basic.- Les réponses sont paresseuses :
request()retourne tout de suite ; le corps n’est récupéré qu’àgetContent()/toArray()(ce qui permet des requêtes concurrentes).- Retries avec
RetryableHttpClient; clients scopés par hôte (base_uri, en-têtes/auth par défaut) ; clients typés injectables.- Pour les tests :
MockHttpClientsimule les réponses (aucun appel réseau).- C’est du serveur-à-serveur : Symfony consomme WordPress/Moodle/Stripe/n’importe quelle API.
- Pour un dev Next.js :
HttpClient≈fetch/axios/undici, mais avec scoping, retries et mock intégrés.
🎯 Objectifs
- Faire des requêtes HTTP авec options (query, json, auth).
- Exploiter les réponses paresseuses et la concurrence.
- Configurer des clients scopés/typés et des retries.
- Simuler les appels en test.
1. Une requête de base
composer require symfony/http-clientuse Symfony\Contracts\HttpClient\HttpClientInterface;
final class WeatherClient
{
public function __construct(private readonly HttpClientInterface $client) {}
public function current(string $city): array
{
$response = $this->client->request('GET', 'https://api.example.com/weather', [
'query' => ['city' => $city], // ?city=...
'headers' => ['Accept' => 'application/json'],
'auth_bearer' => 'MY_API_TOKEN', // Authorization: Bearer ...
]);
return $response->toArray(); // décode le JSON en tableau (déclenche la requête)
}
}Options fréquentes : query (query string), json (corps JSON, ajoute le bon Content-Type), body (corps brut/form), headers, auth_bearer, auth_basic: [$user, $pass], timeout.
2. Réponses paresseuses et concurrence
request() ne bloque pas : elle prépare la requête et rend la main. La requête réseau est réellement effectuée quand on lit la réponse (getContent(), toArray(), getStatusCode()). Conséquence : on peut lancer plusieurs requêtes puis lire les résultats — elles s’exécutent en parallèle.
$responses = [];
foreach ($cities as $city) {
$responses[$city] = $this->client->request('GET', $url, ['query' => ['city' => $city]]);
// pas encore exécutées : on empile
}
foreach ($responses as $city => $response) {
$data[$city] = $response->toArray(); // exécution concurrente au fil des lectures
}C’est le moyen simple d’appeler N endpoints en parallèle sans gérer de promesses — utile pour agréger plusieurs sources (chapitre 13.3-13.4).
3. Gérer les erreurs
Par défaut, lire le corps d’une réponse 4xx/5xx lève une exception. On peut vérifier le statut avant :
$response = $this->client->request('GET', $url);
if ($response->getStatusCode() !== 200) {
// getStatusCode() ne lève pas ; getContent() lèverait sur 4xx/5xx
throw new \RuntimeException('API externe indisponible : ' . $response->getStatusCode());
}Les exceptions du composant (TransportExceptionInterface, ClientExceptionInterface 4xx, ServerExceptionInterface 5xx) permettent un try/catch ciblé.
4. Retries
Les API externes ont des hoquets (5xx, timeouts). On enveloppe le client avec RetryableHttpClient pour réessayer automatiquement (avec backoff) :
# config/packages/framework.yaml
framework:
http_client:
default_options:
max_retries: 3 # réessaie les échecs transitoires
retry_failed:
delay: 1000
multiplier: 25. Clients scopés et typés
Appeler la même API à plusieurs endroits en répétant l’URL de base, l’auth et les en-têtes est fastidieux. On configure un client scopé par hôte :
framework:
http_client:
scoped_clients:
moodle.client:
base_uri: '%env(MOODLE_URL)%'
query: { moodlewsrestformat: 'json' }
wordpress.client:
base_uri: '%env(WP_URL)%'
auth_basic: ['%env(WP_USER)%', '%env(WP_APP_PASSWORD)%']On injecte le client scopé par son nom (#[Autowire]) ou, mieux, on crée un client typé (une classe qui encapsule l’API) :
final class MoodleClient
{
public function __construct(
#[Autowire(service: 'moodle.client')] // le client scopé
private readonly HttpClientInterface $client,
) {}
public function coreCourseGetCourses(): array
{
return $this->client->request('GET', '/webservice/rest/server.php', [
'query' => ['wsfunction' => 'core_course_get_courses', 'wstoken' => '...'],
])->toArray();
}
}Ce client typé (MoodleClient, WordPressClient) est la brique des chapitres 13.3-13.4 : il encapsule l’API externe derrière des méthodes métier claires, injectables et testables.
6. Mocker en test
En test, on n’appelle pas le vrai service : MockHttpClient renvoie des réponses simulées.
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
$mock = new MockHttpClient([
new MockResponse(json_encode(['temp' => 21]), ['http_code' => 200]),
]);
$client = new WeatherClient($mock);
// $client->current('Paris') renvoie ['temp' => 21] sans réseauC’est indispensable pour des tests rapides et déterministes (Partie 14).
💡 Pour un dev Next.js :
HttpClient≈fetch/undici, mais avec le scoping (comme une instanceaxios.create({ baseURL })), les retries intégrés, la concurrence paresseuse et le mock natif. Comme pour la conso d’API dans Next.js (Partie 11.8), c’est du serveur-à-serveur : ici Symfony est le client d’autres services (WordPress, Moodle, Stripe…). Encapsulez chaque API tierce dans un client typé — vous ne dispersez jamais des URLs en dur.
✏️ Exercices
Exercice 1. Écrivez une méthode qui GET https://api.example.com/users/{id} avec un Bearer token et renvoie le tableau décodé.
✅ Solution
public function getUser(int $id): array
{
return $this->client->request('GET', "https://api.example.com/users/$id", [
'auth_bearer' => $this->token,
'headers' => ['Accept' => 'application/json'],
])->toArray();
}toArray() décode le JSON (et lève sur 4xx/5xx). Idéalement, token et l’URL de base viennent d’un client scopé.
Exercice 2. Comment appeler 10 endpoints en parallèle avec HttpClient ?
✅ Solution
Grâce aux réponses paresseuses : lancer les 10 request() (elles ne bloquent pas, on les empile dans un tableau), puis itérer pour lire (toArray()/getContent()) — les requêtes s’exécutent concurremment au fil des lectures. Pas besoin de gérer des promesses ; le multiplexage est géré par le composant.
Exercice 3. Pourquoi encapsuler une API externe dans un client typé (ex. MoodleClient) plutôt que d’appeler HttpClient partout ?
✅ Solution
Pour centraliser l’URL de base, l’auth, les en-têtes et la gestion d’erreurs à un endroit, exposer des méthodes métier claires (getCourses()) au lieu d’URLs dispersées, faciliter le test (mock du client typé) et le remplacement. C’est le même principe que les services (Partie 5) : on dépend d’une abstraction de l’API externe, pas de détails HTTP éparpillés.
🧠 Quiz de révision
1. Comment fait-on une requête GET avec query params et Bearer ?
$client->request('GET', $url, ['query' => [...], 'auth_bearer' => $token]), puis ->toArray()/->getContent().
2. Pourquoi les réponses sont-elles paresseuses ?
request() retourne immédiatement ; la requête réseau se déclenche à la lecture. Cela permet d’exécuter plusieurs requêtes en parallèle.
3. Qu’est-ce qu’un client scopé ?
Un client préconfiguré pour un hôte (base_uri, en-têtes/auth par défaut), qu’on injecte par son nom — évite de répéter la config à chaque appel.
4. Comment ajouter des retries automatiques ?
Via RetryableHttpClient / la config http_client.default_options.max_retries (avec backoff) — réessaie les échecs transitoires.
5. Comment tester du code qui appelle une API externe ?
Avec MockHttpClient (+ MockResponse) : des réponses simulées, sans appel réseau (tests rapides et déterministes).
Prochain chapitre : 13.2 — Webhooks : recevoir & émettre — recevoir proprement et notifier des tiers.