HTTP Client (PSR-18)

Info: PSR-18 Compliant: Fully implements Psr\Http\Client\ClientInterface for maximum interoperability.

Overview

The HTTP Client provides a clean API for making outbound HTTP requests:

Basic Usage

GET Request

use Larafony\Framework\Http\Client\CurlHttpClient;
use Larafony\Framework\Http\Factories\RequestFactory;

$client = new CurlHttpClient();
$requestFactory = new RequestFactory();

// Create GET request
$request = $requestFactory->createRequest('GET', 'https://api.github.com/users/octocat');

// Send request
$response = $client->sendRequest($request);

// Get response data
echo $response->getStatusCode(); // 200
echo $response->getBody(); // JSON response

POST Request with JSON

use Larafony\Framework\Http\Factories\StreamFactory;

$streamFactory = new StreamFactory();

// Create POST request
$request = $requestFactory->createRequest('POST', 'https://api.example.com/users')
 ->withHeader('Content-Type', 'application/json')
 ->withBody($streamFactory->createStream(json_encode([
'name' => 'John Doe',
'email' => 'john@example.com'
])));

$response = $client->sendRequest($request);
$data = json_decode($response->getBody(), true);

Configuration

Client Configuration

use Larafony\Framework\Http\Client\Config\HttpClientConfig;

// Custom configuration
$config = new HttpClientConfig(
timeout: 30, // Request timeout in seconds
followRedirects: true, // Follow HTTP redirects
maxRedirects: 5, // Maximum redirects to follow
verifyPeer: true, // Verify SSL certificate
verifyHost: true, // Verify SSL host
);

$client = new CurlHttpClient($config);

Convenience Factory Methods

// Quick configurations
$config = HttpClientConfig::withTimeout(60);
$config = HttpClientConfig::insecure(); // Disable SSL verification (dev only!)
$config = HttpClientConfig::withProxy('proxy.local:8080', 'user:pass');

Making Requests

Different HTTP Methods

// GET
$request = $requestFactory->createRequest('GET', 'https://api.example.com/users');

// POST
$request = $requestFactory->createRequest('POST', 'https://api.example.com/users');

// PUT
$request = $requestFactory->createRequest('PUT', 'https://api.example.com/users/1');

// PATCH
$request = $requestFactory->createRequest('PATCH', 'https://api.example.com/users/1');

// DELETE
$request = $requestFactory->createRequest('DELETE', 'https://api.example.com/users/1');

Adding Headers

$request = $requestFactory->createRequest('GET', 'https://api.example.com/data')
 ->withHeader('Authorization', 'Bearer ' . $token)
 ->withHeader('Accept', 'application/json')
 ->withHeader('User-Agent', 'LarafonyApp/1.0');

Query Parameters

// Build URL with query parameters
$url = 'https://api.example.com/search?' . http_build_query([
'q' => 'larafony',
'page' => 1,
'per_page' => 20
]);

$request = $requestFactory->createRequest('GET', $url);

Response Handling

Reading Response

$response = $client->sendRequest($request);

// Status code
$statusCode = $response->getStatusCode(); // 200

// Headers
$contentType = $response->getHeaderLine('Content-Type');
$allHeaders = $response->getHeaders();

// Body
$body = $response->getBody()->getContents();

// JSON response
$data = json_decode($body, true);

Checking Status

if ($response->getStatusCode() === 200) {
// Success
}

if ($response->getStatusCode() >= 400) {
// Error
}

Error Handling

Network Errors

use Larafony\Framework\Http\Client\Exceptions\ClientError;
use Larafony\Framework\Http\Client\Exceptions\ConnectionError;
use Larafony\Framework\Http\Client\Exceptions\TimeoutError;

try {
$response = $client->sendRequest($request);
} catch (TimeoutError $e) {
// Request timed out
echo "Request timed out";
} catch (ConnectionError $e) {
// Connection failed
echo "Connection failed: " . $e->getMessage();
} catch (ClientError $e) {
// Other client errors
echo "Client error: " . $e->getMessage();
}

HTTP Status Errors

use Larafony\Framework\Http\Client\Exceptions\NotFoundError;
use Larafony\Framework\Http\Client\Exceptions\UnauthorizedError;

try {
$response = $client->sendRequest($request);
} catch (UnauthorizedError $e) {
// 401 Unauthorized
echo "Authentication required";
} catch (NotFoundError $e) {
// 404 Not Found
echo "Resource not found";
}

Testing with MockHttpClient

Creating Mock Responses

use Larafony\Framework\Http\Client\MockHttpClient;
use Larafony\Framework\Http\Factories\ResponseFactory;

// Create mock response
$mockResponse = (new ResponseFactory())
 ->createResponse(200)
 ->withHeader('Content-Type', 'application/json')
 ->withJson(['id' => 1, 'name' => 'Test User']);

// Create mock client
$client = new MockHttpClient($mockResponse);

// Use in tests - no actual HTTP request is made
$response = $client->sendRequest($anyRequest);
// Always returns the mocked response

Testing Service with HTTP Client

class GitHubService
{
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactory $requestFactory
) {}

public function getUser(string $username): array
{
$request = $this->requestFactory->createRequest(
'GET',
"https://api.github.com/users/{$username}"
);

$response = $this->httpClient->sendRequest($request);
return json_decode($response->getBody(), true);
}
}

// In tests
$mockResponse = (new ResponseFactory())
 ->createResponse(200)
 ->withJson(['login' => 'octocat', 'name' => 'The Octocat']);

$mockClient = new MockHttpClient($mockResponse);
$service = new GitHubService($mockClient, new RequestFactory());

$user = $service->getUser('octocat'); // Uses mock, no network call
assert($user['login'] === 'octocat');

Practical Examples

Example 1: API Client

class WeatherApiClient
{
public function __construct(
private readonly ClientInterface $httpClient,
private readonly string $apiKey
) {}

public function getCurrentWeather(string $city): array
{
$url = 'https://api.weather.com/current?' . http_build_query([
'city' => $city,
'apikey' => $this->apiKey
]);

$request = (new RequestFactory())
->createRequest('GET', $url)
->withHeader('Accept', 'application/json');

try {
$response = $this->httpClient->sendRequest($request);
return json_decode($response->getBody(), true);
} catch (ClientError $e) {
throw new WeatherApiException('Failed to fetch weather', 0, $e);
}
}
}

Example 2: Webhook Sender

class WebhookService
{
public function __construct(
private readonly ClientInterface $httpClient
) {}

public function sendWebhook(string $url, array $data): bool
{
$request = (new RequestFactory())
->createRequest('POST', $url)
->withHeader('Content-Type', 'application/json')
->withBody(
(new StreamFactory())->createStream(json_encode($data))
);

try {
$response = $this->httpClient->sendRequest($request);
return $response->getStatusCode() === 200;
} catch (ClientError $e) {
Log::error('Webhook failed', [
'url' => $url,
'error' => $e->getMessage()
]);
return false;
}
}
}

Best Practices

Do

Don't

Next Steps

Logging

Log HTTP requests and responses for debugging.

Read Guide

Configuration

Store API keys and endpoints in configuration.

Read Guide