chuyskywalker/rolling-curl
Efficient curl_multi wrapper for fetching many URLs in parallel without overwhelming servers. Maintains a fixed number of simultaneous connections, rolling new requests in as others finish, with optional per-request callbacks to process responses as they arrive.
Installation:
composer require chuyskywalker/rolling-curl
Add to composer.json under require:
"chuyskywalker/rolling-curl": "*"
Basic Usage:
use RollingCurl\RollingCurl;
$rollingCurl = new RollingCurl();
$rollingCurl->get('https://example.com/api/1')
->get('https://example.com/api/2')
->setSimultaneousLimit(5)
->setCallback(function($request, $rollingCurl) {
// Process response here
})
->execute();
First Use Case:
Replace a blocking file_get_contents() loop with RollingCurl for fetching multiple API endpoints or web pages concurrently. Example:
$urls = ['https://api.example.com/users', 'https://api.example.com/posts'];
$rollingCurl = new RollingCurl();
foreach ($urls as $url) {
$rollingCurl->get($url);
}
$rollingCurl->setSimultaneousLimit(3)->execute();
examples/ for real-world patterns (e.g., scraping, API polling).get(), post(), put(), delete() for request types.setSimultaneousLimit() to control concurrency.setCallback() for per-request processing.execute() to start the rolling queue.Chain requests fluently before execution:
$rollingCurl = new RollingCurl();
$rollingCurl->get('https://api.example.com/data')
->post('https://api.example.com/submit', ['key' => 'value'])
->setSimultaneousLimit(5);
Process responses as they complete (avoids memory buildup):
$rollingCurl->setCallback(function($request, $rollingCurl) {
$data = json_decode($request->getResponseText(), true);
// Store in DB, queue a job, etc.
$rollingCurl->clearCompleted(); // Free memory
});
Loop through URLs with dynamic options:
$urls = ['url1', 'url2', 'url3'];
foreach ($urls as $url) {
$request = new \RollingCurl\Request($url);
$request->addOptions([CURLOPT_TIMEOUT => 10]);
$rollingCurl->add($request);
}
execute() in a Job to avoid blocking:
use Illuminate\Bus\Queueable;
use RollingCurl\RollingCurl;
class FetchUrlsJob extends Job {
use Queueable;
public function handle() {
$rollingCurl = new RollingCurl();
// ... add requests ...
$rollingCurl->execute();
}
}
RollingCurl to Laravel’s container:
$this->app->singleton(RollingCurl::class, function() {
return new RollingCurl();
});
Catch failures in the callback:
$rollingCurl->setCallback(function($request) {
if ($request->getError()) {
Log::error("Failed: " . $request->getError());
// Retry logic or dead-letter queue
}
});
RollingCurl.setSimultaneousLimit(10) to avoid DOS.DOMDocument).Model::create() or queue a StoreScrapedDataJob.for ($page = 1; $page <= 10; $page++) {
$rollingCurl->get("https://api.example.com/data?page=$page");
}
Retry-After headers by pausing the queue:
$rollingCurl->setCallback(function($request) {
if ($request->getResponseHeader('Retry-After')) {
sleep((int) $request->getResponseHeader('Retry-After'));
}
});
Artisan::command('scrape:urls', function() {
$rollingCurl = new RollingCurl();
// ... add requests ...
$rollingCurl->execute();
});
schedule:run).Avoid Memory Leaks:
clearCompleted() and prunePendingRequestQueue() in the callback.Laravel HTTP Client Bridge:
If using Laravel’s HttpClient, convert RollingCurl responses to Illuminate\Http\Client\Response:
$rollingCurl->setCallback(function($request) {
$response = new \Illuminate\Http\Client\Response(
$request->getResponseText(),
$request->getStatusCode(),
$request->getResponseHeaders()
);
// Use Laravel's response methods (e.g., $response->json())
});
Testing:
RollingCurl in PHPUnit:
$mock = Mockery::mock(RollingCurl::class);
$mock->shouldReceive('execute')->andReturnSelf();
$mock->shouldReceive('get')->andReturnSelf();
Logging:
$rollingCurl->setCallback(function($request) {
Log::info("Fetched {$request->getUrl()}: {$request->getStatusCode()}");
});
Stale Codebase:
curl_multi_* functions. Workaround:
// Polyfill for PHP 8.1+
if (!function_exists('curl_multi_errno')) {
function curl_multi_errno($mh) { /* ... */ }
}
addOptions(), risking invalid curlopt values. Validate options:
$validOptions = [CURLOPT_TIMEOUT, CURLOPT_HEADER, /* ... */];
if (!in_array($option, $validOptions)) {
throw new \InvalidArgumentException("Invalid cURL option");
}
Memory Growth:
prunePendingRequestQueue(), the pending request list grows indefinitely. Call it in the callback:
$rollingCurl->setCallback(function($request, $rollingCurl) {
$rollingCurl->prunePendingRequestQueue();
});
$request->addOptions([CURLOPT_WRITEFUNCTION => function($ch, $data) {
file_put_contents('output.txt', $data, FILE_APPEND);
return strlen($data);
}]);
Callback Timing:
execute(). Avoid relying on callback order for sequential logic.Synchronized wrapper or queue results to a database.Connection Limits:
simultaneousLimit is unlimited if not set. Always configure it:
$rollingCurl->setSimultaneousLimit(5); // Critical for production!
Error Handling:
curl_multi_* errors may not propagate. Check $request->getError() in the callback.$rollingCurl->setCallback(function($request) {
if ($request->getError() && $attempts < 3) {
$rollingCurl->add($request); // Requeue
}
});
Laravel-Specific Issues:
How can I help you explore Laravel packages today?