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:
- PSR-18 Compliant - Standard HTTP client interface
- Native CurlHandle - No external dependencies
- Configurable - Timeouts, SSL, proxy, redirects
- Testable - MockHttpClient for testing
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 responsePOST 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 responseTesting 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
- Use dependency injection for HTTP client
- Set appropriate timeouts for external APIs
- Handle network errors gracefully
- Use MockHttpClient in tests
- Add retry logic for transient failures
- Log failed requests for debugging
Don't
- Don't disable SSL verification in production
- Don't ignore network errors
- Don't use infinite timeouts
- Don't make HTTP calls in loops without rate limiting
