eypsilon / curler
Many/Curler | Another one CURLs the dust
2.0.0
2026-07-22 12:09 UTC
Requires
- php: >=8.0
README
A modern, type-safe HTTP client for PHP 8.1+ with chainable API, pipeline callbacks, and support for the latest HTTP standards.
✨ Features
- 🚀 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
📦 Installation
composer require eypsilon/curler
🚀 Quick Start
use 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');
📚 Documentation
Configuration
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', ]);
Basic Requests
// 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');
HTTP QUERY Method (RFC 9204)
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 );
Authentication
// 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');
Headers & Query Parameters
// 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();
Response Pipeline (Callbacks)
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');
Image Conversion
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...
Error Handling
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 Object
$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');
Request History
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();
Utility Methods
// 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"
🔧 Advanced Examples
Custom CURL Options
$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');
Retry with Custom Status Codes
$response = (new Curler()) ->withRetry( attempts: 5, delayMs: 200, onStatus: [429, 503, 504] // Only retry on these statuses ) ->get('https://rate-limited-api.example.com');
Building Complex Queries
$response = (new Curler()) ->url('https://api.example.com/search') ->query([ 'q' => 'php', 'sort' => 'stars', 'order' => 'desc' ]) ->authBearer('token') ->withRetry(3) ->get();
1. The Responder (Server-Side)
<?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) );
2. The Receiver (Client-Side)
<?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(); }
3. Complete Example with Both Sides
<?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>
4. With .htaccess Protection (Apache)
# .htaccess - Schützt den Endpoint AuthType Basic AuthName "Restricted Area" AuthUserFile /path/to/.htpasswd Require valid-user
5. Or with PHP Built-in Auth
<?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);
🔄 Migration from v1.x
| 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() |
🤝 Contributing
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
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Engin Ypsilon