A modern, type-safe HTTP client for PHP 8.1+ with chainable API, pipeline callbacks, and support for the latest HTTP standards.
- 🚀 Chainable API - Clean, fluent interface for all HTTP operations
- 🔒 Type-safe - Full PHP 8.1+ type declarations
- 📦 HTTP/2 Support - Default protocol for better performance
- 🔄 QUERY Method - RFC 9204 support for safe, idempotent queries with body
- 🔗 Pipeline Callbacks - Chain multiple transformations with validation
- 🖼️ Image to Data URI - Automatic base64 conversion for images
- 🔁 Retry Logic - Configurable retries with backoff
- 📊 Meta Information - Duration, size, timestamps for each request
- 🎯 Authentication - Basic, Bearer, Digest, API Key
- 📝 Request History - Track all requests with timing
- ⚡ PSR-18 Ready - Compatible with modern PHP standards
composer require eypsilon/curleruse Many\Http\Curler;
// Simple GET request
$response = (new Curler())->get('https://api.example.com/users');
echo $response->getBody();
// With authentication and JSON
$response = (new Curler())
->authBearer('your-token-here')
->withJson(['name' => 'John Doe'])
->post('https://api.example.com/users');
// With callbacks and validation
$response = (new Curler())
->validate(fn($data) => !empty($data), 'Data not empty')
->jsonDecode()
->through(fn($data) => array_merge($data, ['processed' => true]))
->jsonEncode(JSON_PRETTY_PRINT)
->get('https://api.example.com/data');Set global configuration before making requests:
Curler::setConfig([
// Default URL prefix (auto-applied to relative URLs)
'default_url' => 'https://api.example.com',
// Default headers sent with every request
'default_headers' => [
'X-App-Name' => 'MyApp',
'Accept' => 'application/json',
],
// Default CURL options
'default_options' => [
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'MyApp/1.0',
],
// Features
'enable_exceptions' => true, // Throw exceptions on errors
'enable_meta' => true, // Include meta data in response
'enable_history' => true, // Track request history
// Image conversion (MIME types to convert to data URIs)
'image_to_data' => ['image/jpeg', 'image/png', 'image/webp'],
// Retry configuration
'retry_attempts' => 3,
'retry_delay_ms' => 100,
'retry_on_status' => [429, 500, 502, 503, 504],
// Date format for timestamps
'date_format' => 'Y-m-d H:i:s.u',
]);// GET
$response = (new Curler())->get('https://api.example.com/users');
// GET with query parameters
$response = (new Curler())->get('https://api.example.com/users', [
'page' => 1,
'limit' => 10
]);
// POST with form data
$response = (new Curler())
->withForm(['name' => 'John', 'email' => 'john@example.com'])
->post('https://api.example.com/users');
// POST with JSON
$response = (new Curler())
->withJson(['name' => 'John', 'email' => 'john@example.com'])
->post('https://api.example.com/users');
// PUT, PATCH, DELETE
$response = (new Curler())
->withJson(['name' => 'Jane'])
->put('https://api.example.com/users/123');
$response = (new Curler())->delete('https://api.example.com/users/123');The QUERY method is perfect for complex queries that exceed URL length limits:
// Simple QUERY with JSON body
$response = (new Curler())->query(
'https://api.example.com/search',
['query' => 'php', 'filters' => ['status' => 'active']]
);
// QUERY with GraphQL
$response = (new Curler())->queryGraphQL(
'https://api.example.com/graphql',
'query GetUser($id: ID!) { user(id: $id) { name email } }',
['id' => '123']
);
// QUERY with URL parameters + body
$response = (new Curler())->query(
'https://api.example.com/search',
['query' => 'php'],
['page' => 1, 'limit' => 10] // URL query params
);// Basic Auth
$response = (new Curler())
->authBasic('username', 'password')
->get('https://api.example.com/protected');
// Bearer Token
$response = (new Curler())
->authBearer('your-token-here')
->get('https://api.example.com/protected');
// Digest Auth
$response = (new Curler())
->authDigest('username', 'password')
->get('https://api.example.com/protected');
// API Key (custom header)
$response = (new Curler())
->authApiKey('your-api-key', 'X-API-Key')
->get('https://api.example.com/protected');// Set headers
$response = (new Curler())
->withHeader('X-Custom', 'value')
->withHeaders([
'X-Header-1' => 'value1',
'X-Header-2' => 'value2',
])
->get('https://api.example.com');
// Set query parameters (chainable)
$response = (new Curler())
->url('https://api.example.com/users')
->query(['page' => 1, 'limit' => 10])
->mergeQuery(['sort' => 'desc'])
->get();Transform responses through a pipeline of callbacks with validation:
$response = (new Curler())
// Validate first
->validate(fn($data) => !empty($data), 'Data not empty')
->validate(fn($data) => Curler::isJson($data), 'Must be JSON')
// Transform
->jsonDecode()
->through(fn($data) => array_merge($data, ['processed_at' => date('c')]))
->through(fn($data) => array_filter($data))
->jsonEncode(JSON_PRETTY_PRINT)
// Final validation
->validate(fn($data) => Curler::isJson($data), 'Final must be JSON')
->get('https://api.example.com/data');Automatically convert images to data URIs:
Curler::setConfig([
'image_to_data' => ['image/jpeg', 'image/png', 'image/webp'],
'default_url' => 'https://upload.wikimedia.org/wikipedia/commons/thumb',
]);
$response = (new Curler())->get('/path/to/image.jpg');
// $response->getBody() contains: data:image/jpeg;base64,/9j/4AAQ...use Many\Http\Curler;
use Many\Exception\AppCallbackException;
try {
$response = (new Curler())
->withExceptions(true)
->withRetry(3, 200) // Retry 3 times with 200ms delay
->timeout(30)
->get('https://api.example.com/unreliable');
if ($response->isSuccess()) {
echo $response->getBody();
}
} catch (AppCallbackException $e) {
echo "Pipeline error: " . $e->getMessage();
echo "Stage: " . $e->getStage();
echo "Context: " . print_r($e->getContext(), true);
} catch (RuntimeException $e) {
echo "HTTP error: " . $e->getMessage();
}$response = (new Curler())->get('https://api.example.com/data');
// Check status
if ($response->isSuccess()) {
$data = $response->getJson(); // Auto-decode JSON
} elseif ($response->isClientError()) {
// 4xx errors
} elseif ($response->isServerError()) {
// 5xx errors
}
// Get response data
$body = $response->getBody(); // Raw body
$json = $response->getJson(); // Decoded JSON
$status = $response->getStatusCode(); // HTTP status
$type = $response->getContentType(); // Content-Type
$size = $response->getSize(); // Size in bytes
// Get meta information (if enabled)
$meta = $response->getMeta();
$duration = $response->getMeta('duration');
$timestamp = $response->getMeta('timestamp');Curler::setConfig(['enable_history' => true]);
// Make some requests...
// Get all requests
$history = Curler::getHistory();
foreach ($history as $request) {
echo sprintf(
"[%s] %s %s (%0.4fs)\n",
$request['method'],
$request['status'],
$request['url'],
$request['duration']
);
}
// Get request count
$total = Curler::getRequestCount();
// Clear history
Curler::clearHistory();// Check if string is valid JSON
if (Curler::isJson($data)) {
// It's JSON!
}
// Format bytes to human-readable size
echo Curler::formatBytes(memory_get_usage()); // "1.23 MB"
// Get timestamp with microseconds
echo Curler::dateMicroSeconds(); // "2026-07-22 08:42:51.988100"
// Get difference between two timestamps
$diff = Curler::dateMicroDiff('2026-07-22 08:00:00', '2026-07-22 08:00:05');
echo $diff; // "05.000000"$response = (new Curler())
->withOptions([
CURLOPT_SSL_VERIFYPEER => false, // NOT recommended for production
CURLOPT_VERBOSE => true,
CURLOPT_PROXY => 'proxy.example.com:8080',
])
->get('https://api.example.com');$response = (new Curler())
->withRetry(
attempts: 5,
delayMs: 200,
onStatus: [429, 503, 504] // Only retry on these statuses
)
->get('https://rate-limited-api.example.com');$response = (new Curler())
->url('https://api.example.com/search')
->query([
'q' => 'php',
'sort' => 'stars',
'order' => 'desc'
])
->authBearer('token')
->withRetry(3)
->get();<?php
/**
* responder.php - Empfängt und verarbeitet Requests
*
* Dieser Script simuliert eine .htaccess geschützte API-Endpoint.
* Es zeigt die empfangenen Daten und gibt sie als JSON oder print_r zurück.
*/
use Many\Http\Curler;
// Sammle alle Request-Informationen
$response = [
'auth_type' => $_SERVER['AUTH_TYPE'] ?? 'error',
'headers' => getallheaders(),
'body' => file_get_contents('php://input'),
'content_types' => [
'json' => 'application/json',
'form' => 'application/x-www-form-urlencoded',
'text' => 'text/plain',
]
];
// Bestimme den Content-Type basierend auf Query-Parameter
$requestedType = $_GET['c_type'] ?? null;
$charset = $_GET['charset'] ?? 'UTF-8';
$contentType = $response['content_types'][$requestedType] ?? $_SERVER['CONTENT_TYPE'] ?? 'text/html';
// Setze den Content-Type Header
if ($requestedType && isset($response['content_types'][$requestedType])) {
header(sprintf('Content-Type: %s; charset=%s', $contentType, $charset));
}
// Parse den Body wenn möglich
if ($requestedType === 'form' && ctype_print($response['body'])) {
parse_str($response['body'], $response['body_parsed']);
}
// Gib die Response aus
exit($contentType === 'application/json'
? json_encode($response, JSON_PRETTY_PRINT)
: print_r($response, true)
);<?php
/**
* receiver.php - Sendet Requests an den Responder
*
* Zeigt wie Curler authentifizierte Requests an den geschützten Endpoint sendet.
*/
use Many\Http\Curler;
try {
// Moderne Curler Syntax
$response = (new Curler())
->authBasic('many', '123456') // Oder authDigest() für Digest Auth
->withForm([
'lorem_ipsum' => 'Dolor Sit Amet',
'timestamp' => Curler::dateMicroSeconds(),
])
->jsonDecode()
->post('https://example.com/restricted/?c_type=json&charset=UTF-8');
// Ausgabe der Response
printf(
'<pre>Status: %d%s%s</pre>',
$response->getStatusCode(),
PHP_EOL,
$response->isSuccess()
? json_encode($response->getJson(), JSON_PRETTY_PRINT)
: 'Error: ' . $response->getBody()
);
} catch (Exception $e) {
echo 'Request failed: ' . $e->getMessage();
}<?php
/**
* Vollständiges Beispiel: Receiver + Responder
*
* Zeigt einen kompletten Request/Response-Cycle mit
* Authentifizierung, Datenübertragung und Verarbeitung.
*/
use Many\Http\Curler;
// 1. Konfiguration für den Responder (Server)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['responder'])) {
$response = [
'status' => 'ok',
'message' => 'Data received successfully',
'received_data' => $_POST,
'headers' => getallheaders(),
'auth_type' => $_SERVER['AUTH_TYPE'] ?? 'none',
'timestamp' => Curler::dateMicroSeconds(),
];
// Authentifizierung prüfen
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!str_contains($auth, 'Basic bWFueToxMjM0NTY=')) {
http_response_code(401);
$response['status'] = 'error';
$response['message'] = 'Unauthorized';
}
header('Content-Type: application/json');
exit(json_encode($response, JSON_PRETTY_PRINT));
}
// 2. Receiver (Client) - Sendet den Request
if (isset($_GET['receiver'])) {
try {
$response = (new Curler())
->authBasic('many', '123456')
->withForm([
'name' => 'John Doe',
'email' => 'john@example.com',
'message' => 'Hello from Curler!'
])
->withQuery(['responder' => 1])
->jsonDecode()
->post('https://example.com/restricted/');
if ($response->isSuccess()) {
$data = $response->getJson();
echo "✅ Success: " . ($data['message'] ?? 'OK') . "\n";
echo "📦 Data: " . print_r($data['received_data'] ?? [], true);
} else {
echo "❌ Error: HTTP " . $response->getStatusCode() . "\n";
echo $response->getBody();
}
} catch (Exception $e) {
echo "❌ Request failed: " . $e->getMessage();
}
exit;
}
// 3. Demo HTML Interface
?>
<!DOCTYPE html>
<html>
<head>
<title>Curler Receiver/Responder Demo</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow: auto; }
.buttons { display: flex; gap: 1rem; margin: 1rem 0; }
button { padding: 0.5rem 1rem; cursor: pointer; }
</style>
</head>
<body>
<h1>🔄 Curler Receiver/Responder Demo</h1>
<p>Dieses Beispiel zeigt, wie Curler sowohl als Client (Receiver)
als auch als Server (Responder) fungiert.</p>
<div class="buttons">
<a href="?receiver=1"><button>📤 Send Request (Receiver)</button></a>
<a href="?responder=1"><button>📥 View Responder</button></a>
</div>
<?php
// 4. Live Request mit Curler
try {
$response = (new Curler())
->authBasic('many', '123456')
->withForm([
'lorem_ipsum' => 'Dolor Sit Amet',
'source' => 'Curler Demo'
])
->withQuery(['responder' => 1])
->jsonDecode()
->post('https://example.com/restricted/');
echo "<h2>📡 Live Response</h2>";
echo "<pre>" . json_encode($response->getJson(), JSON_PRETTY_PRINT) . "</pre>";
} catch (Exception $e) {
echo "<h2>❌ Error</h2>";
echo "<pre>" . htmlspecialchars($e->getMessage()) . "</pre>";
}
?>
</body>
</html># .htaccess - Schützt den Endpoint
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /path/to/.htpasswd
Require valid-user<?php
// responder.php - Mit PHP-eigener Authentifizierung
$validUsers = [
'many' => '123456',
'admin' => 'admin123'
];
// Prüfe Basic Auth
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="Restricted"');
http_response_code(401);
exit('Authentication required');
}
$user = $_SERVER['PHP_AUTH_USER'];
$pass = $_SERVER['PHP_AUTH_PW'];
if (!isset($validUsers[$user]) || $validUsers[$user] !== $pass) {
http_response_code(401);
exit('Invalid credentials');
}
// Auth erfolgreich - verarbeite Request
$data = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$response = [
'status' => 'ok',
'user' => $user,
'received' => $data,
'timestamp' => Curler::dateMicroSeconds(),
];
header('Content-Type: application/json');
echo json_encode($response, JSON_PRETTY_PRINT);| Old | New |
|---|---|
->callback('function') |
->through('function') |
->callbackIf(['is_string'], 'trim') |
->validate(fn($d) => is_string($d))->through('trim') |
->htmlChars() |
->through(fn($d) => htmlspecialchars($d)) |
$result['response'] |
$response->getBody() |
$result['http_code'] |
$response->getStatusCode() |
$result['meta'] |
$response->getMeta() |
getCurlCount() |
getRequestCount() |
getCurlTrace() |
getHistory() |
readableBytes() |
formatBytes() |
postFields() |
withForm() or withJson() |
setOpt() |
withOption() |
setOptArray() |
withOptions() |
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Engin Ypsilon