symfony/http-client-contracts
Symfony HttpClient Contracts provides stable interfaces for HTTP clients and responses, extracted from Symfony. Build libraries against these battle-tested abstractions and swap implementations easily while staying compatible with Symfony’s HttpClient ecosystem.
## Getting Started
### Minimal Setup
1. **Install the package** (interfaces only):
```bash
composer require symfony/http-client-contracts
Pair with a concrete client (choose one):
composer require symfony/http-client
composer require guzzlehttp/guzzle guzzlehttp/psr7
composer require php-http/discovery
First use case: Replace hardcoded HTTP calls in a Laravel service with the contract:
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiService
{
public function __construct(private HttpClientInterface $client) {}
public function fetchData(string $endpoint): array
{
$response = $this->client->request('GET', "https://api.example.com/$endpoint");
return $response->toArray(); // Returns decoded JSON
}
}
Register the client in Laravel:
Add to config/services.php:
'http_client' => [
'default' => Symfony\Component\HttpClient\HttpClient::class,
],
Bind the interface in AppServiceProvider:
$this->app->bind(HttpClientInterface::class, function ($app) {
return new HttpClient();
});
HttpClientInterface in constructors and let Laravel resolve it via the bound concrete client.
// In a controller or service
public function __construct(private HttpClientInterface $client) {}
$client = HttpClient::create([
'base_uri' => 'https://api.example.com/v1/',
'headers' => ['Authorization' => 'Bearer ' . $token],
]);
$response = $client->request('GET', '/users');
$status = $response->getStatusCode(); // 200, 404, etc.
$content = $response->getContent(false); // Raw body
$data = $response->toArray(); // Decoded JSON
$headers = $response->getHeaders(); // ['content-type' => ['application/json']]
try {
$response = $client->request('GET', '/protected', [
'throw' => true, // Throws HttpClientException on non-2xx
]);
} catch (HttpClientException $e) {
logger()->error('API request failed', ['error' => $e->getMessage()]);
}
HttpClientInterface with MockResponse:
$mockResponse = MockResponse::fromJsonString('{"status": "success"}');
$mockClient = $this->createMock(HttpClientInterface::class);
$mockClient->method('request')->willReturn($mockResponse);
TestHttpClient (from symfony/http-client):
use Symfony\Component\HttpClient\Test\MockHttpClient;
$client = new MockHttpClient([
'GET /users' => ['status' => 200, 'body' => '{"id": 1}'],
]);
class RetryHttpClient implements HttpClientInterface
{
public function __construct(private HttpClientInterface $client) {}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$retries = $options['retries'] ?? 3;
$lastException = null;
for ($i = 0; $i < $retries; $i++) {
try {
return $this->client->request($method, $url, $options);
} catch (HttpClientException $e) {
$lastException = $e;
sleep(2 ** $i); // Exponential backoff
}
}
throw $lastException;
}
}
AsyncHttpClient or PSR-18 async clients:
$asyncClient = new AsyncHttpClient();
$promise = $asyncClient->request('GET', 'https://api.example.com');
$response = $promise->await(); // Blocks until response
Missing Concrete Client:
Class 'Symfony\Component\HttpClient\HttpClient' not found.symfony/http-client or another PSR-18-compatible client.HttpClientInterface is bound in AppServiceProvider.Response Method Assumptions:
toArray(), getContent(), and getHeaders() throw exceptions for non-2xx responses.['throw' => false]:
$response = $client->request('GET', '/error', ['throw' => false]);
if ($response->getStatusCode() !== 200) {
// Handle error
}
Async Confusion:
ResponseInterface::until() and cancel(), but basic request() calls are synchronous unless using an async client.symfony/http-client's AsyncHttpClient or guzzlehttp/promises.Laravel Facade Collisions:
HttpClientInterface with Laravel’s Http facade (which uses Guzzle under the hood).HttpClientInterface for consistency.$client = HttpClient::create([
'debug' => true, // Logs all requests/responses
]);
$response = $client->request('GET', '/debug');
logger()->debug('Response Headers', $response->getHeaders());
logger()->debug('Response Body', $response->getContent());
$client = HttpClient::create([
'pooling' => true, // Reuses connections for multiple requests
]);
$client->request('GET', '/slow', [
'timeout' => 5.0, // Seconds
]);
Custom Response Decorators:
ResponseInterface for domain-specific logic:
class ApiResponse implements ResponseInterface
{
public function __construct(private ResponseInterface $response) {}
public function toDomainModel(): User
{
$data = $this->response->toArray();
return new User($data['id'], $data['name']);
}
}
PSR-18 Interoperability:
symfony/psr-http-message-bridge to bridge Symfony messages with PSR-7/18:
composer require symfony/psr-http-message-bridge
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Psr\Http\Message\RequestInterface;
$symfonyRequest = new SymfonyRequest();
$psrRequest = $symfonyRequest->toPsrRequest();
Laravel-Specific Integrations:
Laravel\HttpClient\AsyncHttpClient (Laravel 10+) for async support:
use Laravel\HttpClient\AsyncHttpClient;
$asyncClient = new AsyncHttpClient();
Http client to HttpClientInterface (if needed):
$this->app->bind(HttpClientInterface::class, function () {
return Http::client(); // Laravel 9+
});
$client = HttpClient::create([
'base_uri' => 'https://api.example.com/v1/',
]);
$response = $client->request('GET', '/users'); // Resolves to https://api.example.com/v1/users
.env:
$client = HttpClient::create([
'headers' => ['Authorization' => 'Bearer ' . env('API_TOKEN')],
]);
How can I help you explore Laravel packages today?