Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MANY/CURLER | Another one CURLs the dust

A modern, type-safe HTTP client for PHP 8.1+ with chainable API, pipeline callbacks, and support for the latest HTTP standards.

PHP Version License Packagist

✨ 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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.


Vercel Demo GitHub

Author

Engin Ypsilon

Releases

Used by

Contributors

Languages