guzzlehttp/ringphp
RingPHP provides a low-level API for sending HTTP requests in PHP, with pluggable handlers for cURL, streams, and mock transports. It powers Guzzle's request layer and offers a simple, extensible interface for building custom HTTP clients.
RingPHP is archived (last release 2018-07-31) and is not a dependency you should add to a new Laravel app. You are almost certainly here because a legacy package pulled it in transitively — typically Guzzle 5.x, or an older SDK that pinned guzzlehttp/guzzle: ~5.0. Confirm with:
composer why guzzlehttp/ringphp
composer show guzzlehttp/ringphp
If nothing depends on it, remove it. If something does, pin it (composer require guzzlehttp/ringphp:^1.1) and plan a migration rather than new work on top of it.
Where to look first:
README.md — the "Simple API" section is the entire contract: a handler is a callable function (array $request): array $response.src/Core.php — static helpers for headers, URLs, bodies, success checks.src/Client/CurlHandler.php, StreamHandler.php, MockHandler.php, CurlMultiHandler.php — the concrete transports.src/Middleware.php — mapRequest, mapResponse, history.First use case — a single request against a transport directly, no Guzzle involved:
use GuzzleHttp\Ring\Client\CurlHandler;
use GuzzleHttp\Ring\Core;
$handler = new CurlHandler();
$response = $handler([
'http_method' => 'GET',
'scheme' => 'https',
'uri' => '/posts/1',
'headers' => [
'Host' => ['jsonplaceholder.typicode.com'],
'Accept' => ['application/json'],
],
'client' => [
'curl' => [CURLOPT_TIMEOUT => 5],
],
]);
if (isset($response['error'])) {
throw $response['error']; // transport-level failure
}
$body = Core::body($response); // reads the stream into a string
That is the package in one snippet: an array in, an array out, errors returned rather than thrown.
Wrap a handler with defaults instead of repeating request keys. Core::wrap() merges base client options and is the idiomatic place for a base URL, default headers, and timeouts:
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = Core::wrap(new CurlHandler(), [
'client' => [
'curl' => [CURLOPT_CONNECTTIMEOUT => 2],
'base_url' => 'https://api.example.com',
],
]);
Core::url($request) then resolves relative URIs against base_url, and Core::applyOptions() merges per-request client overrides.
Compose middleware as decorators. Every middleware is just a callable that takes a handler and returns a handler, so they nest in either direction and stack cleanly:
use GuzzleHttp\Ring\Middleware;
$handler = new CurlHandler();
// Record every exchange for debugging or audit logs.
$handler = Middleware::history($handler, $container);
// Add auth + correlation id to every request.
$handler = Middleware::mapRequest($handler, function (array $request) {
$request['headers']['Authorization'] = ['Bearer ' . config('services.api.token')];
$request['headers']['X-Request-Id'] = [(string) Str::uuid()];
return $request;
});
// Normalize JSON responses into arrays, leaving errors untouched.
$handler = Middleware::mapResponse($handler, function (array $response) {
if (!isset($response['error']) && $response['status'] === 200) {
$response['json'] = json_decode(Core::body($response), true);
}
return $response;
});
Bind the handler as a singleton in a service provider. RingPHP ships no framework integration; wiring is yours. Registering a single wrapped handler lets you swap transports per environment (curl in prod, mock in tests) through the container:
$this->app->singleton('api.handler', fn () => /* composed handler above */);
Use MockHandler for tests. It takes a queue of response arrays and satisfies the same callable contract, so tests exercise your middleware and mapping code without touching the network:
use GuzzleHttp\Ring\Client\MockHandler;
$handler = new MockHandler([
['status' => 200, 'headers' => ['Content-Type' => ['application/json']], 'body' => '{"id":1}'],
['status' => 500, 'body' => ''],
]);
Parallel fan-out with CurlMultiHandler. Unlike CurlHandler, it returns a FutureArrayInterface. This is the one capability RingPHP still does well and the main reason legacy code holds onto it:
$multi = new CurlMultiHandler();
$futures = collect($urls)->map(fn ($u) => $multi->__invoke([...]));
foreach ($futures as $f) { $f->wait(); } // or ->then(...)
Choose the transport deliberately. CurlHandler needs ext-curl and is fastest; StreamHandler works with only allow_url_fopen and is the fallback when curl is unavailable. Do not rely on behavioral parity between them — see below.
"repositories" + a fork) if you must run it on PHP 8.1+, and budget a migration. The modern equivalent for everything here is Guzzle 7's GuzzleHttp\HandlerStack + PSR-7/PSR-18; Laravel's Http facade already sits on it. Map Middleware::mapRequest → HandlerStack::push middleware, and $response['status']/Core::body() → $response->status()/(string) $response->getBody().isset($response['error']) before reading status; error responses have no status key at all. The value is an exception (ConnectException, CurlException), and HTTP 4xx/5xx is a perfectly normal response.headers key may be missing entirely on empty responses. Use Core::hasHeader() / Core::headerLines() rather than indexing directly.body is a stream resource for curl/stream handlers but a plain string for MockHandler. Never assume one or the other — always funnel through Core::body($response), which handles both and rewinds.client is the only key transports read for configuration, and the only place curl options, base_url, verify, and connect_timeout should live. Raw CURLOPT_* arrays are passed straight through, so an accidental CURLOPT_HEADER => true corrupts body parsing; and CURLOPT_* options are ignored entirely by StreamHandler.follow_location vs CURLOPT_FOLLOWLOCATION, timeout vs CURLOPT_TIMEOUT). If you support both, test both — or hard-require ext-curl and delete the stream path.Authorization headers and bodies; scrub before persisting, and disable it outside debug.$container from Middleware::history to inspect the exact request array your middleware produced versus what the transport sent — most "wrong URL/header" bugs are middleware ordering, since decorators apply in reverse of wrapping order.GuzzleHttp\Ring\Client\CurlFactory instead.ringphp and guzzlehttp/guzzle:^6|^7 cannot coexist. If a transitive dependency forces both, use Composer replace/provide shims only as a temporary bridge and treat it as a migration signal.How can I help you explore Laravel packages today?