SF
{ }
Symfony · HttpClient · API-Integration · PHP 8.4
Symfony HttpClient:
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.

16 Min. Lesezeit Scoped Clients · Retry · Concurrency · Mocking · Fehlerbehandlung Symfony 7.x · PHP 8.4 · HttpClient 7.x

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.

11. FAQ: Symfony HttpClient

1Was ist Symfony HttpClient?
HTTP-Client-Komponente für Symfony: typsichere, asynchrone Requests, Scoped Clients, automatische Retries und MockHttpClient für vollständig isolierte Tests ohne Netzwerk.
2HttpClient vs. Guzzle?
HttpClient ist tiefer im Symfony-Container integriert: Scoped Clients, Retry-Decorator und MockHttpClient sind eingebaut. Bei Guzzle muss man diese Patterns selbst aufbauen.
3Was ist ein Scoped Client?
Vorkonfigurierter Service mit fixer Base-URL, Auth-Header und Timeouts — einmal in framework.yaml definiert, überall per Autowire injizierbar. Kein API-Key im aufrufenden Code.
4MockHttpClient in Tests?
Implementiert HttpClientInterface und gibt MockResponse-Objekte zurück statt echter HTTP-Calls. Per Constructor Injection statt des echten Clients nutzbar — kein HTTP-Server nötig.
5Automatische Retries aktivieren?
retry_failed in framework.yaml unter dem Scoped Client: max_retries, delay, multiplier und http_codes. RetryableHttpClient kann auch manuell als Decorator eingesetzt werden.
6Parallele Requests?
Mehrere request()-Aufrufe absenden, dann mit stream() über die Responses iterieren. Die Wartezeit läuft parallel — kein async/await, normales synchrones PHP.
7HTTP-Fehler behandeln?
Exception-Hierarchie: ClientExceptionInterface (4xx), ServerExceptionInterface (5xx), TransportExceptionInterface (Netzwerk). Alle geben Zugriff auf Body und Headers der Response.
8Ohne Symfony Framework nutzbar?
Ja. symfony/http-client ist ein eigenständiges Paket und funktioniert in jedem PHP-Projekt. Service-Container-Integration und Scoped Clients sind Extras für Symfony-Projekte.
9Große Antworten streamen?
toStream() liefert die Response als PHP-Stream-Ressource. Zeilenweise oder chunk-weise lesen ohne die gesamte Antwort im RAM zu halten — ideal für CSV-Exporte und große JSON-Payloads.
10Welches Backend wird genutzt?
Mit ext-curl: CurlHttpClient mit curl_multi für echte Concurrency. Ohne cURL: NativeHttpClient mit PHP-Streams. Der Wechsel ist transparent — das Interface bleibt identisch.