HTTP Client (PSR-18)

Make HTTP requests to external APIs with Larafony's PSR-18 compliant HTTP client.

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