HttpClient — компонент Symfony для отправки HTTP-запросов. Поддерживает async, streaming, retry, scoping и интеграцию с PSR-18. На экзамене проверяют знание API и паттернов использования.
Базовое использование
<?php
declare(strict_types=1);
use Symfony\Contracts\HttpClient\HttpClientInterface;
class GitHubService
{
public function __construct(
private readonly HttpClientInterface $httpClient,
) {
}
public function getUser(string $username): array
{
$response = $this->httpClient->request('GET', "https://api.github.com/users/{$username}", [
'headers' => [
'Accept' => 'application/vnd.github.v3+json',
],
]);
// Status code
$statusCode = $response->getStatusCode(); // 200
// Response headers
$contentType = $response->getHeaders()['content-type'][0];
// Decoded JSON (auto-detects from Content-Type)
$data = $response->toArray();
return $data;
}
}
Request Options
<?php
declare(strict_types=1);
// GET with query parameters
$response = $httpClient->request('GET', 'https://api.example.com/search', [
'query' => [
'q' => 'symfony',
'page' => 1,
'limit' => 20,
],
]);
// Sends: GET /search?q=symfony&page=1&limit=20
// POST with JSON body
$response = $httpClient->request('POST', 'https://api.example.com/products', [
'json' => [
'name' => 'Widget',
'price' => 9.99,
],
// Automatically sets Content-Type: application/json
]);
// POST with form data
$response = $httpClient->request('POST', 'https://api.example.com/login', [
'body' => [
'username' => 'admin',
'password' => 'secret',
],
// Sends: application/x-www-form-urlencoded
]);
// Custom headers
$response = $httpClient->request('GET', 'https://api.example.com/data', [
'headers' => [
'Authorization' => 'Bearer token123',
'X-Custom-Header' => 'value',
],
]);
// Timeout configuration
$response = $httpClient->request('GET', 'https://slow-api.example.com/', [
'timeout' => 10.0, // Total request timeout (seconds)
'max_duration' => 30.0, // Max time for the whole request
]);
Все доступные опции
| Опция | Тип | Описание |
|---|---|---|
headers |
array | Заголовки запроса |
query |
array | Query string параметры |
body |
string/array | Тело запроса |
json |
mixed | JSON-тело (auto Content-Type) |
auth_basic |
string | HTTP Basic auth (user:password) |
auth_bearer |
string | Bearer token |
timeout |
float | Таймаут соединения (сек) |
max_duration |
float | Макс. длительность запроса |
max_redirects |
int | Макс. число редиректов (20) |
base_uri |
string | Базовый URL |
verify_peer |
bool | Проверять SSL-сертификат |
proxy |
string | HTTP-прокси |
Scoped Clients
Scoped clients позволяют настроить клиент для конкретного API с предустановленными параметрами.
# config/packages/framework.yaml
framework:
http_client:
scoped_clients:
github.client:
base_uri: 'https://api.github.com'
headers:
Accept: 'application/vnd.github.v3+json'
Authorization: 'Bearer %env(GITHUB_TOKEN)%'
stripe.client:
base_uri: 'https://api.stripe.com/v1'
auth_basic: '%env(STRIPE_SECRET_KEY)%:'
<?php
declare(strict_types=1);
use Symfony\Contracts\HttpClient\HttpClientInterface;
class GitHubService
{
// Symfony auto-wires the scoped client by variable name
public function __construct(
private readonly HttpClientInterface $githubClient,
) {
}
public function getRepos(): array
{
// Base URI and headers are pre-configured
$response = $this->githubClient->request('GET', '/user/repos');
return $response->toArray();
}
}
Подвох экзамена: Scoped client автоматически инжектится по имени переменной в конструкторе. Если scoped client называется
github.client, то переменная должна быть$githubClient(camelCase без точки). Symfony автоматически маппитgithub.client→$githubClient.
Асинхронные запросы
<?php
declare(strict_types=1);
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class AsyncFetcher
{
public function __construct(
private readonly HttpClientInterface $httpClient,
) {
}
public function fetchMultiple(array $urls): array
{
// Start all requests concurrently
$responses = [];
foreach ($urls as $key => $url) {
$responses[$key] = $this->httpClient->request('GET', $url);
}
// Responses are lazy — actual HTTP call happens on first access
$results = [];
foreach ($responses as $key => $response) {
$results[$key] = $response->toArray();
}
return $results;
}
// Stream responses as they arrive
public function fetchStream(array $urls): \Generator
{
$responses = [];
foreach ($urls as $url) {
$responses[] = $this->httpClient->request('GET', $url);
}
// stream() yields chunks as they arrive
foreach ($this->httpClient->stream($responses) as $response => $chunk) {
if ($chunk->isFirst()) {
// First chunk — headers available
yield 'status' => $response->getStatusCode();
}
if ($chunk->isLast()) {
// Last chunk — response complete
yield 'data' => $response->toArray();
}
}
}
}
Подвох экзамена: HttpClient в Symfony ленивый (lazy).
request()НЕ отправляет HTTP-запрос немедленно. Реальный запрос происходит при первом обращении к ответу (getStatusCode(),toArray(), etc.). Это позволяет запустить несколько запросов параллельно.
Retry Failed Requests
<?php
declare(strict_types=1);
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
// Programmatic retry configuration
$retryStrategy = new GenericRetryStrategy(
delayMs: 1000, // Initial delay: 1 second
multiplier: 2.0, // Exponential backoff
maxDelayMs: 30000, // Max delay: 30 seconds
jitter: 0.3, // Random jitter: ±30%
);
$client = new RetryableHttpClient(
$httpClient,
$retryStrategy,
maxRetries: 3,
);
# config/packages/framework.yaml
framework:
http_client:
default_options:
retry_failed:
enabled: true
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 30000
jitter: 0.3
http_codes:
- 423 # Locked
- 425 # Too Early
- 429 # Too Many Requests
- 500 # Internal Server Error
- 502 # Bad Gateway
- 503 # Service Unavailable
Symfony 8.0: В Symfony 8.0 retry configuration поддерживает
retry_on_exceptionдля указания типов исключений, при которых нужен retry. Также добавлена поддержкаRetry-Afterheader — клиент автоматически ждёт указанное время.
Error Handling
<?php
declare(strict_types=1);
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
try {
$response = $httpClient->request('GET', 'https://api.example.com/data');
$data = $response->toArray();
} catch (ClientExceptionInterface $e) {
// 4xx errors (client error)
$statusCode = $e->getResponse()->getStatusCode();
} catch (ServerExceptionInterface $e) {
// 5xx errors (server error)
$statusCode = $e->getResponse()->getStatusCode();
} catch (RedirectionExceptionInterface $e) {
// 3xx errors (too many redirects)
} catch (TransportExceptionInterface $e) {
// Network errors (timeout, DNS failure, etc.)
}
// Alternative: check status without exception
$response = $httpClient->request('GET', 'https://api.example.com/data');
$statusCode = $response->getStatusCode(); // No exception for 4xx/5xx HERE
// Exception is thrown on content access methods:
// toArray(), getContent(), getHeaders() throw on 3xx/4xx/5xx
// Use throw: false to disable
$response = $httpClient->request('GET', 'https://api.example.com/data');
$content = $response->getContent(false); // No exception even for 4xx/5xx
Подвох экзамена:
getStatusCode()НИКОГДА не бросает исключения (даже для 4xx/5xx). Исключения бросаются приtoArray(),getContent(),getHeaders()для ошибочных статусов. Передайтеfalseаргументом чтобы отключить это поведение.
Тестирование HttpClient
<?php
declare(strict_types=1);
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
// Create mock client for tests
$mockResponses = [
new MockResponse(json_encode(['id' => 1, 'name' => 'Widget']), [
'http_code' => 200,
'response_headers' => ['Content-Type: application/json'],
]),
new MockResponse('Not Found', [
'http_code' => 404,
]),
];
$client = new MockHttpClient($mockResponses);
// First request returns 200 with JSON
$response = $client->request('GET', '/products/1');
$data = $response->toArray(); // ['id' => 1, 'name' => 'Widget']
// Second request returns 404
$response = $client->request('GET', '/products/999');
$statusCode = $response->getStatusCode(); // 404
<?php
declare(strict_types=1);
// Callback-based mock for dynamic responses
$client = new MockHttpClient(function (string $method, string $url): MockResponse {
if (str_contains($url, '/products/')) {
return new MockResponse(json_encode(['found' => true]), [
'http_code' => 200,
]);
}
return new MockResponse('', ['http_code' => 404]);
});