The Krystal HTTP Client is a robust HTTP client built on top of cURL. It provides a clean, object-oriented interface for making HTTP requests with comprehensive error handling and configuration options.
method(string $url, array $data = [], array $extra = [])
$url (string): The target URL$data (array): Data to send (query params for GET/HEAD, body for others)$extra (array): Additional cURL options (CURLOPT_* constants as keys)<?php
use Krystal\Http\Client\HttpClient;
$client = new HttpClient();
// cURL constants => values, if required
$client->setDefaultOptions([
]);
$response = $client->get('https://api.example.com/users');
// GET with query parameters
$response = $client->get('https://api.example.com/users', [
'page' => 1,
'limit' => 20
]);
// POST request with form data
$response = $client->post('https://api.example.com/login', [
'username' => 'john',
'password' => 'secret'
]);
// PUT request
$response = $client->put('https://api.example.com/users/1', [
'name' => 'John Updated',
'email' => 'john@example.com'
]);
// PATCH request
$response = $client->patch('https://api.example.com/users/1', [
'name' => 'John Modified'
]);
// DELETE request
$response = $client->delete('https://api.example.com/users/1');
// HEAD request (returns headers only)
$headers = $client->head('https://api.example.com/users');
// Generic request method
$response = $client->request('POST', 'https://api.example.com/users', $data);
Add custom HTTP headers to your requests:
$response = $client->get('https://api.example.com/protected', [], [
CURLOPT_HTTPHEADER => [
'Authorization: Bearer token123',
'X-Custom-Header: value'
]
]);
Send JSON data with automatic Content-Type header configuration for methods supporting request bodies.
Parameters:
$method (string): HTTP method - POST, PUT, PATCH, or DELETE
$url (string): Target URL endpoint
$data (array): Data to encode as JSON request body (default: empty array)
$extra (array): Additional cURL options to merge (default: empty array)
Returns: HttpResponse instance
Throws: InvalidArgumentException for invalid methods or JSON encoding failures
Basic usage:
// Send POST JSON request
$client->jsonRequest('POST', '/api/users', ['name' => 'John']);
// Send PUT JSON request
$client->jsonRequest('PUT', '/api/users/123', ['name' => 'John Updated']);
// Send PATCH JSON request
$client->jsonRequest('PATCH', '/api/users/123', ['email' => 'new@example.com']);
// Send DELETE with JSON body (if your API requires it)
$client->jsonRequest('DELETE', '/api/users/123', ['reason' => 'inactive']);
With extra headers:
// Add custom headers
$response = $client->jsonRequest('POST', '/api/auth/login', [
'username' => 'admin',
'password' => 'secret'
], [
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
'X-Custom-Header: value'
]
]);
// Headers to be sent: Content-Type, Accept, X-API-Key, X-Custom-Header
Upload files using multipart/form-data:
$response = $client->post('https://api.example.com/upload', [], [
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('/path/to/file.jpg', 'image/jpeg', 'photo.jpg'),
'description' => 'My photo'
]
]);
Download binary files (images, archives, PDFs, etc.) directly to disk with streaming support and full exception-based error handling.
The download() method streams the response body directly to a local file, avoiding loading the entire file into memory – ideal for large downloads.
<?php
use Krystal\Http\Client\HttpClient;
$client = new HttpClient();
$from = 'https://example.com/files/large-archive.zip';
$to = __DIR__ . '/path/to/downloads/archive.zip';
try {
$client->download($from,$to);
echo "File downloaded successfully!";
} catch (\RuntimeException $e) {
echo "Download failed: " . $e->getMessage();
}
With custom headers (e.g., authentication):
$client->download($from, $to, [
CURLOPT_HTTPHEADER => [
'Authorization: Bearer your-token-here',
'Accept: application/pdf'
]
]
);
Many APIs return large result sets in pages (e.g. 100 items per request). The processPaginatedResponse() method simplifies fetching all pages automatically. It iterates through pages, calls your callback for each page's items, and handles both GET and POST-based pagination.
This method does not collect items automatically — your callback receives the items of each page and decides what to do (merge into array, save to database, process, etc.).
processPaginatedResponse(array $config, callable $callback): void
Parameters:
$config (array) — All settings in one place (required keys marked with *)$callback (callable) — Required function called for each page Signature: function(array $itemsData, int $page): voidThrows:
$client->processPaginatedResponse([
'url' => 'https://api.example.com/products',
'per_page' => 50,
'data_key' => 'products',
'total_pages_key' => 'totalPages',
'extra' => [
CURLOPT_HTTPHEADER => ['Authorization: Bearer your-token']
],
], function (array $items, $page) use (&$allProducts) {
$allProducts = array_merge($allProducts, $items);
echo "Processed page $page (" . count($items) . " items)\n";
});
$client->processPaginatedResponse([
'url' => 'https://api.crm.com/search',
'method' => 'POST',
'per_page' => 200,
'per_page_param'=> 'page_size',
'page_param' => 'page_number',
'payload' => [
'filter' => ['status' => 'active'],
'sort' => 'desc'
],
'data_key' => 'records',
'total_count_key' => 'total_records',
], function (array $records, $page) {
foreach ($records as $record) {
// saveToDatabase($record);
}
echo "Saved page $page\n";
});
$client->processPaginatedResponse([
'url' => 'https://api.service.com/list',
'per_page' => 30,
'per_page_param' => 'count', // API uses "count" instead of "per_page"
'page_param' => 'offset', // API uses offset instead of page
'data_key' => 'items',
'next_page_key' => 'nextLink', // API-specific key
], function ($items, $page) use (&$collection) {
$collection = array_merge($collection, $items);
});
This method makes it easy to work with virtually any paginated JSON API — whether it uses page numbers, total counts, next URLs, or custom parameter names.
The HTTP client supports automatic retries for failed requests. Retries are applied globally (configured once at construction time) and work for both regular requests and file downloads.
Retries are triggered on:
Features include:
Retry is disabled by default. Enable and customize it via the constructor:
<?php
use Krystal\Http\Client\HttpClient;
// Basic enable with defaults (3 attempts, default statuses & backoff)
$client = new HttpClient([], [
'enabled' => true,
]);
// Customized retry settings
$client = new HttpClient([], [
'enabled' => true,
'maxRetries' => 5,
'retryStatuses' => [429, 503], // only rate-limit & service unavailable
'backoffStrategy' => [1, 4, 10, 30, 60], // seconds before each retry
'addJitter' => true, // adds small random variation
]);
// Exponential backoff example
$client = new HttpClient([], [
'enabled' => true,
'maxRetries' => 4,
'backoffStrategy' => function ($attempt) {
return min(120, (int) pow(2, $attempt)); // 2, 4, 8, 16 seconds (capped at 120)
},
'addJitter' => true,
]);
$client = new HttpClient([], [
'enabled' => true,
'maxRetries' => 4,
'retryStatuses' => [429, 502, 503, 504],
]);
// This request will be automatically retried up to 4 times if it fails with 429/5xx or connection error
$response = $client->get('https://api.example.com/data');
// File download will also retry automatically
$client->download(
'https://example.com/large-file.zip',
__DIR__ . '/backup.zip'
);
Note: Retries add delay to failed requests. Use conservative backoff values in production to avoid long hangs. For non-idempotent methods (POST/PATCH) retries are still performed — if your API doesn't support safe retries on these methods, consider disabling retry globally or handling failures manually.
CurlMulti is a wrapper around PHP's curl_multi_* functions that provides a clean, object-oriented interface for concurrent HTTP requests. It manages multiple Curl instances and executes them in parallel.
Important: CurlMulti works with Curl instances directly, not HttpClient instances. You'll need to use the low-level Curl class.
Basic example:
<?php
use Krystal\Http\Client\Curl;
use Krystal\Http\Client\CurlMulti;
use RuntimeException;
// Create individual Curl instances
$curl1 = new Curl();
$curl2 = new Curl();
$curl3 = new Curl();
// Configure each Curl instance
$curl1->setOption(CURLOPT_URL, 'https://api.example.com/users/1');
$curl1->setOption(CURLOPT_RETURNTRANSFER, true);
$curl2->setOption(CURLOPT_URL, 'https://api.example.com/users/2');
$curl2->setOption(CURLOPT_RETURNTRANSFER, true);
$curl3->setOption(CURLOPT_URL, 'https://api.example.com/users/3');
$curl3->setOption(CURLOPT_RETURNTRANSFER, true);
// Create multi-handle
$multi = new CurlMulti();
try {
// Add all Curl instances to multi-handle
$multi->add($curl1);
$multi->add($curl2);
$multi->add($curl3);
// Execute all requests concurrently
$results = $multi->exec();
// Process results
foreach ($results as $id => $result) {
echo "Result $id:\n";
echo "Content: " . $result['result'] . "\n";
echo "HTTP Code: " . ($result['info']['http_code'] ?? 'N/A') . "\n";
if ($result['errno']) {
echo "Error: " . $result['error'] . "\n";
}
}
} catch (RuntimeException $e) {
echo 'Multi-request failed: ' . $e->getMessage();
} finally {
// Clean up
$curl1->close();
$curl2->close();
$curl3->close();
}
While echo $response outputs the body directly, additional methods provide detailed access to all response information.
The HttpResponse class encapsulates HTTP response data from cURL requests, providing methods to access response body, headers, status codes, and request metadata.
You can retrieve the HTTP response content in several ways, depending on your needs:
$response->getBody(); // Returns the raw response body as a string
$response->parseJSON(); // Parses JSON and returns an associative array
$response->parseXML(); // Parses XML and returns an associative array
Key details
\RuntimeException if the body is empty, not valid JSON, or parsing fails.RuntimeException if the body is empty, not valid XML, or parsing fails.Both parseJSON() and parseXML() provide safe, convenient access to structured data while giving you meaningful exceptions on malformed or unexpected content.
Return HTTP response status as integer (200, 404, 500, etc.).
$response->getStatusCode(); // Returns int
Check for successful response. Determine if status code is in 2xx range (200-299).
$response->isSuccessful(); // Returns bool
Check for redirect response. Determine if status code is in 3xx range (300-399).
$response->isRedirect(); // Returns bool
Determine if status code is in 4xx range (400-499), indicating client-side issues.
$response->hasClientError(); // Returns bool
Determine if status code is in 5xx range (500-599), indicating server-side issues.
$response->hasServerError(); // Returns bool
Determine if request failed due to HTTP error (4xx/5xx) or cURL error.
$response->hasFailed(); // Returns bool
Return all headers as key-value array.
$response->getHeaders(); // Returns array
Get header value using case-insensitive lookup.
$response->getHeader('Content-Type'); // Returns string|null
Return cURL error information array or null if successful.
$response->getError(); // Returns array|null
Return complete cURL transfer metadata array.
$response->getInfo(); // Returns array
Return ultimate URL after following any redirects (if any).
$response->getEffectiveUrl(); // Returns string|null
Return total request duration in seconds including DNS, connection, and transfer.
$response->getTotalTime(); // Returns float|null
// Make request
$response = $client->get('https://api.example.com/data');
if ($response->isSuccessful()) {
$content = $response->getBody(); // Get response content
$status = $response->getStatusCode(); // Get status code
$contentType = $response->getHeader('Content-Type'); // Get header
} else {
echo "Request failed with status: " . $response->getStatusCode();
}
It is strongly recommended to create a custom API client class when interacting with external APIs. This approach provides several benefits:
Implementation example
<?php
use Krystal\Http\Client\HttpClient;
/**
* Custom API client for interacting with JSON-based APIs.
*
* This class assumes the external API communicates exclusively via JSON:
*
* - Requests with payloads are sent as JSON.
* - All responses are expected to be valid JSON strings that are automatically decoded.
*
* Public methods always return decoded associative arrays.
*/
final class ApiClient
{
/** @var string Base URL for the API */
private const BASE_URL = 'https://example.com/api/v1';
/** @var HttpClient Instance of the HTTP client used for requests */
private $httpClient;
/**
* State initialization
*
* @param HttpClient|null $httpClient Optional custom HTTP client. If not provided, a new instance will be created.
*/
public function __construct(HttpClient $httpClient = null)
{
$this->httpClient = $httpClient ?: new HttpClient();
}
/**
* Fetch all books.
*
* @return array Decoded list of books (associative arrays)
*/
public function getBooks()
{
$response = $this->httpClient->get(self::BASE_URL . '/books/all');
return $response->parseJSON();
}
/**
* Fetch a single book by ID.
*
* @param int|string $id The book identifier
* @return array
*/
public function getBook($id)
{
$response = $this->httpClient->get(self::BASE_URL . '/books/single/' . $id);
return $response->parseJSON();
}
/**
* Delete a book by ID.
*
* @param int|string $id The book identifier
* @return array
*/
public function deleteBook($id)
{
$response = $this->httpClient->post(self::BASE_URL . '/books/delete/' . $id);
return $response->parseJSON();
}
/**
* Add a new book.
*
* @param array $book Associative array of book data
* @return array Decoded response (typically the created book or success message)
*/
public function addBook(array $book)
{
$response = $this->httpClient->jsonRequest(self::BASE_URL . '/books/add', $book);
return $response->parseJSON();
}
}
Usage example
<?php
$apiClient = new ApiClient();
$books = $apiClient->getBooks(); // Returns array of associative arrays
$book = $apiClient->getBook(42); // Single book as associative array
print_r($books);
// Add a book
$result = $apiClient->addBook(array(
'title' => 'New Book',
'author' => 'Jane Doe',
));
var_dump($result);