API-Anfragen typsicher und resilient
Wer externe APIs in PHP mit cURL oder Guzzle anspricht, baut oft fragile Integrationen ohne Retry-Logik, ohne sauberes Mocking in Tests und ohne Typsicherheit. Symfony HttpClient liefert all das als Teil des Frameworks — mit Scoped Clients, automatischen Retries, asynchroner Concurrency und einem MockHttpClient für vollständig isolierte Unit-Tests.
Inhaltsverzeichnis
- 1. Warum Symfony HttpClient statt Guzzle oder cURL
- 2. Installation und Grundkonfiguration
- 3. Scoped Clients: API-Konfiguration gebündelt
- 4. Typsichere API-Wrapper mit DTOs
- 5. Retry-Logik und Fehlerbehandlung
- 6. Asynchrone Anfragen und Concurrency
- 7. Mocking mit MockHttpClient in Tests
- 8. Streaming großer Antworten
- 9. HttpClient-Implementierungen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Symfony HttpClient statt Guzzle oder cURL
Externe API-Integrationen sind einer der häufigsten Schmerzbereiche in PHP-Projekten. Mit rohem cURL schreibt man 30 Zeilen boilerplate, bevor auch nur der erste Request abgesetzt wird. Mit Guzzle kommt man schneller ans Ziel, hat aber eine externe Abhängigkeit ohne Symfony-Integration — Konfiguration über Service-Container, Scoped Clients und automatisches Mocking in Tests muss man von Hand bauen. Symfony HttpClient löst das Problem als Teil des Frameworks: eine saubere, typsichere API, die cURL und native PHP-Streams als Backend unterstützt und vollständig in den Symfony Service Container integriert ist.
Der entscheidende Unterschied liegt in der Architektur: Symfony HttpClient ist standardmäßig asynchron. Requests werden erst dann blockierend ausgewertet, wenn man auf die Response zugreift — nicht beim Absenden. Das ermöglicht echte Concurrency ohne Promises oder Callbacks: Man sendet zehn Requests ab und iteriert dann über die Responses, während sie eintreffen. Für Projekte, die mehrere externe APIs ansprechen — Zahlungsanbieter, Versanddienstleister, CRM-Systeme — ist das ein erheblicher Laufzeitvorteil ohne zusätzliche Komplexität.
2. Installation und Grundkonfiguration
Die Installation von Symfony HttpClient ist ein einzelner Composer-Befehl: composer require symfony/http-client. Das Symfony Flex Recipe legt automatisch einen Eintrag in config/packages/framework.yaml an. Die Grundkonfiguration enthält globale Defaults für alle HTTP-Anfragen: maximale Redirect-Anzahl, Verbindungs-Timeout, Request-Timeout, Proxy-Einstellungen und Standard-Header. Diese Defaults gelten für alle Service-Instanzen, die HttpClientInterface injiziert bekommen — es sei denn, ein Scoped Client überschreibt sie für eine spezifische Basis-URL.
Der Service Symfony\Contracts\HttpClient\HttpClientInterface ist nach der Installation automatisch im Container verfügbar und kann per Constructor Injection in jeden Service injiziert werden. Für PHP 8.4-Projekte mit Constructor Property Promotion ist die Injection besonders kompakt. Die Typdeklaration auf HttpClientInterface statt auf die konkrete Implementierung ist wichtig — sie erlaubt das einfache Austauschen der Implementierung (cURL vs. native Streams) und ermöglicht das MockHttpClient-Substitut in Tests ohne weitere Konfiguration.
<?php
declare(strict_types=1);
namespace App\Client;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Base HTTP client wrapper demonstrating constructor injection and typed usage.
*/
final class BaseApiClient
{
public function __construct(
// Inject the specific scoped client by service ID
#[Autowire(service: 'payment.client')]
private readonly HttpClientInterface $client,
) {}
/**
* Perform a GET request and return the decoded JSON body.
*
* @return array<string, mixed>
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface
*/
public function get(string $path, array $query = []): array
{
$response = $this->client->request('GET', $path, [
'query' => $query,
]);
// Status check throws HttpExceptionInterface on 4xx/5xx
$response->getStatusCode();
return $response->toArray();
}
}
3. Scoped Clients: API-Konfiguration gebündelt
Scoped Clients sind das mächtigste Feature von Symfony HttpClient für Projekte mit mehreren externen APIs. Ein Scoped Client ist ein vorkonfigurierter HttpClientInterface-Service mit fester Basis-URL und Default-Optionen: Authentication-Header, API-Keys, Timeouts und Accept-Header werden einmalig konfiguriert und gelten für alle Anfragen dieses Clients. Wenn ein Service einen Payment-API-Client injiziert bekommt, enthält jede Anfrage automatisch den richtigen Authorization-Header — ohne dass der aufrufende Code die API-Credentials kennen oder übergeben muss.
Die Konfiguration erfolgt in config/packages/framework.yaml unter http_client.scoped_clients. Jeder Scoped Client erhält einen Namen, eine Basis-URL, und beliebige Default-Optionen. Im Service Container wird der Scoped Client automatisch als Service registriert — der Name entspricht dem Konfigurationskey. Per #[Autowire(service: 'payment.client')]-Attribut oder einem expliziten Service-Alias wird der richtige Client in den Service injiziert. Das Prinzip ist dasselbe wie bei mehreren Doctrine Entity Managern: ein Interface, mehrere vorkonfigurierte Instanzen.
<?php
// config/packages/framework.yaml — Scoped Client configuration
//
// framework:
// http_client:
// default_options:
// timeout: 30
// max_redirects: 5
// scoped_clients:
// payment.client:
// base_uri: 'https://api.payment-provider.de/v2/'
// headers:
// Authorization: 'Bearer %env(PAYMENT_API_KEY)%'
// Accept: 'application/json'
// timeout: 10
// retry_failed:
// max_retries: 3
// delay: 500
// multiplier: 2
// shipping.client:
// base_uri: 'https://api.shipping.de/rest/'
// auth_basic: ['%env(SHIPPING_USER)%', '%env(SHIPPING_PASS)%']
// timeout: 15
declare(strict_types=1);
namespace App\Client;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Payment API client using a pre-configured scoped HttpClient.
*/
final readonly class PaymentApiClient
{
public function __construct(
#[Autowire(service: 'payment.client')]
private HttpClientInterface $client,
) {}
/**
* Charge a payment and return the transaction ID.
*/
public function charge(string $customerId, int $amountCents, string $currency): string
{
$response = $this->client->request('POST', 'charges', [
'json' => [
'customer_id' => $customerId,
'amount' => $amountCents,
'currency' => $currency,
],
]);
$data = $response->toArray();
return $data['transaction_id'];
}
}
4. Typsichere API-Wrapper mit DTOs
Rohe toArray()-Aufrufe geben array<string, mixed> zurück — ohne Typinformation, ohne IDE-Unterstützung und ohne Sicherheit, dass alle erwarteten Felder vorhanden sind. Der typsichere Ansatz mit Symfony HttpClient ist der Einsatz von DTOs (Data Transfer Objects) als Rückgabetypen. Ein DTO ist eine readonly-Klasse mit typisierten Feldern und einer statischen Factory-Methode, die aus dem Array-Response erstellt wird. PHPStan und die IDE erkennen die Felder, Callers bekommen vollständige Typsicherheit, und Änderungen an der API-Antwortstruktur werden sofort als Typ-Fehler sichtbar.
PHP 8.4 readonly-Klassen sind ideal für API-Response-DTOs: sie sind immutable, brauchen keine Getter-Methoden und sind mit Constructor Property Promotion kompakt deklarierbar. Für komplexe verschachtelte Strukturen kann man DTO-Hierarchien bauen — ein Order-DTO enthält ein Array von OrderLine-DTOs. Die Symfony Serializer-Komponente kann diese Hierarchien automatisch aus JSON-Antworten aufbauen, wenn man den Serializer statt manuellem Array-Zugriff einsetzt. Das macht die Integration mit Symfony HttpClient vollständig typsicher von der HTTP-Schicht bis zur Geschäftslogik.
5. Retry-Logik und Fehlerbehandlung
Symfony HttpClient bringt den RetryableHttpClient mit, der automatisch fehlgeschlagene Requests wiederholt. Er dekoriiert jeden anderen HttpClientInterface-Service und kann über die Scoped-Client-Konfiguration aktiviert werden. Die Retry-Strategie ist konfigurierbar: maximale Anzahl Versuche, Wartezeit zwischen Versuchen, exponentieller Multiplikator und die HTTP-Statuscodes, die einen Retry auslösen (typisch: 429, 500, 502, 503, 504). Transient-Fehler wie Netzwerktimeouts und Rate-Limit-Antworten werden automatisch behandelt — ohne einen einzigen Zeile manuellen Retry-Code.
Für individuelle Retry-Entscheidungen implementiert man RetryDeciderInterface. Die Implementierung entscheidet pro Response und Exception, ob ein Retry sinnvoll ist, und kann Header wie Retry-After auslesen, um die Wartezeit dynamisch anzupassen. Fehlerbehandlung nach dem letzten Retry erfolgt über die Exception-Hierarchie von Symfony HttpClient: TransportExceptionInterface für Netzwerkfehler, RedirectionExceptionInterface für unbehandelte Redirects, ClientExceptionInterface für 4xx-Antworten und ServerExceptionInterface für 5xx-Antworten. Jede Exception enthält die zugehörige Response mit vollem Body, Status und Headers — für präzises Logging und Debugging.
<?php
declare(strict_types=1);
namespace App\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Resilient shipping client with structured error handling and logging.
*/
final readonly class ShippingApiClient
{
public function __construct(
#[Autowire(service: 'shipping.client')]
private HttpClientInterface $client,
private LoggerInterface $logger,
) {}
/**
* Create a shipment label and return the tracking number.
*
* @throws \RuntimeException on permanent failure after retries
*/
public function createLabel(string $orderId, array $address): string
{
try {
$response = $this->client->request('POST', 'labels', [
'json' => ['order_id' => $orderId, 'address' => $address],
]);
return $response->toArray()['tracking_number'];
} catch (ClientExceptionInterface $e) {
// 4xx — log and re-throw without retry (client error, not transient)
$this->logger->error('Shipping API client error', [
'order_id' => $orderId,
'status_code' => $e->getResponse()->getStatusCode(),
'body' => $e->getResponse()->getContent(throw: false),
]);
throw new \RuntimeException('Shipping label creation failed: ' . $e->getMessage(), previous: $e);
} catch (ServerExceptionInterface | TransportExceptionInterface $e) {
// 5xx or network error — RetryableHttpClient has already retried
$this->logger->critical('Shipping API unavailable after retries', ['order_id' => $orderId]);
throw new \RuntimeException('Shipping service temporarily unavailable.', previous: $e);
}
}
}
6. Asynchrone Anfragen und Concurrency
Das Killer-Feature von Symfony HttpClient gegenüber sequentiellem cURL ist die eingebaute Concurrency. Man sendet mehrere Requests ab, ohne auf den ersten zu warten, und iteriert dann über die Responses in der Reihenfolge, in der sie eintreffen. Die Methode stream() akzeptiert ein Array von Responses und liefert sie als Generator — sobald die erste Antwort verfügbar ist, wird sie geliefert, während die anderen noch laufen. So kann man zehn externe API-Aufrufe parallel ausführen, die zusammen nur so lange dauern wie der langsamste Einzelaufruf — statt der Summe aller Wartezeiten.
Ein konkretes Praxisbeispiel: Ein Preisvergleichs-Service muss fünf verschiedene Lieferanten-APIs nach Preisen für dasselbe Produkt befragen. Sequentiell bei je 300 ms Latenz: 1,5 Sekunden. Mit Symfony HttpClient Concurrency: unter 350 ms, weil alle fünf Requests gleichzeitig laufen. Das ist kein async/await, kein Promise-System und keine Event-Loop — es ist das synchrone PHP, das man kennt, aber mit nicht-blockierendem I/O unter der Haube. cURL-Multi oder native Streams als Backend stellen sicher, dass die Concurrency auch bei vielen gleichzeitigen Requests stabil bleibt.
7. Mocking mit MockHttpClient in Tests
Einer der stärksten Aspekte von Symfony HttpClient für die Testbarkeit ist der MockHttpClient. Er implementiert HttpClientInterface und kann anstelle des echten Clients in Tests injiziert werden — ohne Konfigurationsänderungen, ohne HTTP-Server, ohne echte Netzwerkverbindungen. Der MockHttpClient akzeptiert eine Liste von MockResponse-Objekten, die sequentiell zurückgegeben werden, oder eine Callback-Funktion, die pro Request eine Response erzeugt. Das erlaubt präzise Tests: Zeitüberschreitungen simulieren, spezifische Fehlercodes zurückgeben und die gesendeten Request-Daten validieren.
Der MockResponse kann Body, Status-Code und Headers definieren. Für JSON-APIs: new MockResponse(json_encode([...]), ['http_code' => 200]). Für Fehlerszenarien: new MockResponse('', ['http_code' => 503]). Das Callback-Pattern ermöglicht dynamische Responses basierend auf dem Request: URL, Methode, Body und Headers sind im Callback verfügbar. So kann man testen, ob der Service den richtigen Request-Body aufbaut, bevor man die Response verarbeitet — ohne dass ein echter API-Server erreichbar sein muss. Symfony HttpClient-Tests sind dadurch deterministisch, schnell und vollständig isoliert.
<?php
declare(strict_types=1);
namespace App\Tests\Client;
use App\Client\PaymentApiClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
/**
* Tests PaymentApiClient using MockHttpClient — no real HTTP calls.
*/
final class PaymentApiClientTest extends TestCase
{
public function testChargeReturnsTransactionId(): void
{
// Arrange: define what the mock client will return
$mockResponse = new MockResponse(
json_encode(['transaction_id' => 'txn_abc123', 'status' => 'success']),
['http_code' => 200, 'response_headers' => ['Content-Type: application/json']],
);
$mockClient = new MockHttpClient([$mockResponse], 'https://api.payment-provider.de/v2/');
$paymentClient = new PaymentApiClient($mockClient);
// Act
$transactionId = $paymentClient->charge('cust_456', 2999, 'EUR');
// Assert
self::assertSame('txn_abc123', $transactionId);
// Verify the correct request was sent
self::assertSame('POST', $mockResponse->getRequestMethod());
self::assertStringContainsString('charges', $mockResponse->getRequestUrl());
$sentBody = json_decode($mockResponse->getRequestOptions()['body'], true);
self::assertSame('cust_456', $sentBody['customer_id']);
self::assertSame(2999, $sentBody['amount']);
}
public function testChargeHandles503WithRetry(): void
{
// Simulate two 503 responses followed by a successful response
$responses = [
new MockResponse('', ['http_code' => 503]),
new MockResponse('', ['http_code' => 503]),
new MockResponse(json_encode(['transaction_id' => 'txn_retry_ok']), ['http_code' => 200]),
];
$mockClient = new MockHttpClient($responses, 'https://api.payment-provider.de/v2/');
$paymentClient = new PaymentApiClient($mockClient);
$result = $paymentClient->charge('cust_789', 999, 'EUR');
self::assertSame('txn_retry_ok', $result);
}
}
8. Streaming großer Antworten
Wenn externe APIs große Datenmengen zurückgeben — Exportdateien, Produktkataloge, Log-Exports — ist das vollständige Laden der Response in den Arbeitsspeicher keine Option. Symfony HttpClient unterstützt Streaming über die toStream()-Methode, die die Response als PHP-Stream-Ressource zurückgibt. Dieser Stream kann dann zeilenweise oder in Chunks gelesen werden, ohne dass die gesamte Antwort im Speicher gehalten wird. Für CSV-Exporte mit Millionen von Zeilen ist das der einzig praktikable Ansatz.
Ein weiteres Streaming-Szenario ist das direkte Weiterleiten der API-Response an den Browser des Users — etwa beim Download einer generierten PDF-Datei von einem Drittdienst. Mit Symfony HttpClient kann man die Response streamen und dabei die HTTP-Header des Originals — Content-Type, Content-Disposition, Content-Length — an den Symfony-Response-Stream übertragen. Das vermeidet das vollständige Puffern der Datei im PHP-Prozess und skaliert auch bei großen Dateien. Die Chunk-basierte Iteration über den Stream erlaubt dabei, den Transfer jederzeit abzubrechen, wenn der Client die Verbindung trennt.
9. HttpClient-Implementierungen im Vergleich
Je nach Symfony-Installation und Systemkonfiguration stehen verschiedene Symfony HttpClient-Backends zur Verfügung. Die Wahl beeinflusst Concurrency-Verhalten, Performance und Feature-Support.
| Implementierung | Backend | Concurrency | Einsatz |
|---|---|---|---|
| CurlHttpClient | libcurl (ext-curl) | Echte Parallelität via curl_multi | Standard — produktiver Default |
| NativeHttpClient | PHP stream_socket | Keine ext-curl nötig | Fallback ohne cURL-Extension |
| MockHttpClient | Kein Netzwerk | Sequentiell (deterministisch) | Tests — vollständig isoliert |
| TraceableHttpClient | Decorator | Transparente Aufzeichnung | Profiler & Debug-Toolbar |
| RetryableHttpClient | Decorator | Retry mit Backoff | Resiliente API-Calls |
Der TraceableHttpClient ist automatisch im dev-Modus aktiv und zeigt alle HTTP-Anfragen in der Symfony Web Debug Toolbar — mit Status, Dauer, Request-Body und Response-Details. Für die Produktion ist CurlHttpClient der Standard und bietet die beste Performance. Der RetryableHttpClient und der TraceableHttpClient können kombiniert werden — der Decorator-Stack ist beliebig erweiterbar, da alle Implementierungen dasselbe HttpClientInterface implementieren.
Mironsoft
Symfony API-Integration, HttpClient und resiliente Backend-Entwicklung
Externe APIs sicher und resilient anbinden?
Wir entwickeln robuste API-Integrationen mit Symfony HttpClient — typsichere DTOs, Scoped Clients, automatische Retries und vollständig testbare Services für euren Tech-Stack.
API-Integration
Scoped Clients, typsichere DTOs und strukturierte Fehlerbehandlung für externe APIs
Retry & Resilienz
Automatische Retry-Logik, Circuit-Breaker-Patterns und Fallback-Strategien
Test-Setup
MockHttpClient-Integration, vollständige Test-Isolation und CI-fähige Testsuiten
10. Zusammenfassung
Symfony HttpClient ist die richtige Wahl für HTTP-Anfragen in Symfony-Projekten, weil er nicht nur einen HTTP-Request absendet, sondern die gesamte Infrastruktur für resiliente, typsichere und testbare API-Integrationen mitbringt. Scoped Clients bündeln API-Konfiguration im Service-Container. Der RetryableHttpClient behandelt transiente Fehler automatisch. Die eingebaute Concurrency spart Laufzeit bei parallelen API-Calls. Der MockHttpClient macht Tests vollständig deterministisch ohne echte Netzwerke. Und die Exception-Hierarchie erlaubt präzises Fehlerhandling nach Fehlertyp und HTTP-Status.
Der größte praktische Hebel liegt in der Kombination aus Scoped Clients und typsicheren DTO-Wrappern: Jede externe API bekommt einen eigenen Service mit sprechender Schnittstelle, die keine HTTP-Details nach außen durchlässt. Die Geschäftslogik arbeitet mit Domänenobjekten, nicht mit rohen Arrays. Testbarkeit ist von Anfang an eingebaut, nicht nachträglich ergänzt. Das ist der Unterschied zwischen einer Punkt-zu-Punkt-Integration und einer wartbaren Architektur — und Symfony HttpClient macht diesen Unterschied mit wenig Aufwand erreichbar.
Symfony HttpClient — Das Wichtigste auf einen Blick
Scoped Clients
API-Konfiguration einmalig in framework.yaml — Base-URL, Auth-Header und Timeouts gelten für alle Requests des Clients automatisch.
Retry-Logik
RetryableHttpClient wiederholt transiente Fehler mit exponentiellem Backoff — konfigurierbar für Statuscodes, Delays und maximale Versuche.
Concurrency
Mehrere Requests gleichzeitig abschicken und über stream() auswerten — parallele API-Calls ohne async/await oder Event-Loop.
Testing
MockHttpClient mit MockResponse ersetzt den echten Client in Tests — deterministisch, schnell, ohne Netzwerk und ohne echte API-Keys.