yurunsoft/yurun-http
YurunHttp is a feature-rich PHP HTTP client with Curl, Swoole, and Socket drivers. It supports synchronous and asynchronous requests, coroutines, HTTP/2, WebSocket, uploads/downloads, cookies, redirects, proxies, retries, and a simple consistent API for API clients, crawlers, and service integration...
composer require yurunsoft/yurun-http
There is no service provider, facade, or config file — it is a plain PHP library. You instantiate Yurun\Util\HttpRequest and chain options onto it, then call a verb method. The first thing to read is the README's chain-method examples and src/HttpRequest.php, because the public API is the setter list.
Minimal call:
use Yurun\Util\HttpRequest;
$response = (new HttpRequest)
->timeout(10)
->header('Accept', 'application/json')
->get('https://api.example.com/users');
$status = $response->getStatusCode();
$data = $response->json(); // decoded array
$raw = $response->body(); // string
$headers = $response->headers(); // array
Chained POST with automatic JSON encoding:
$response = (new HttpRequest)
->timeout(10)
->header('Authorization', 'Bearer '.$token)
->bodyFormat('json')
->post('https://api.example.com/users', ['name' => 'yurun']);
First realistic use case: as a drop-in replacement for Guzzle in a Laravel app running under Octane/Swoole, where Guzzle's blocking cURL calls pin the worker. Swapping to the Swoole handler lets requests yield the coroutine instead of blocking, with no other change to your call sites.
Bind a configured, transient client in the container. Do not make it a singleton — the instance carries cookies and mutated options.
// AppServiceProvider::register()
$this->app->bind(HttpRequest::class, function () {
return (new HttpRequest)
->timeout(10)
->connectTimeout(3)
->header('User-Agent', 'myapp/1.0');
});
Wrap it behind your own interface, not scattered through controllers. This is the single most valuable pattern, because it is the only way to make the client mockable (see Gotchas) and it gives you one place to add logging, tracing IDs, and metrics.
final class ExampleApi
{
public function __construct(private HttpRequest $http) {}
public function createUser(array $payload): array
{
$res = $this->http->bodyFormat('json')
->post(config('services.example.url').'/users', $payload);
if ($res->getStatusCode() >= 400) {
throw new ApiException($res->body(), $res->getStatusCode());
}
return $res->json() ?? [];
}
}
Choose the handler once, at bootstrap. The handler (cURL vs Swoole) is effectively process-global for HttpRequest defaults; set it in a service provider or bootstrap/app.php, never mid-request:
use Yurun\Util\YurunHttp;
YurunHttp::setDefaultHandler(YurunHttp::HANDLER_SWOOLE);
Fan out with Swoole coroutines — this is where the package earns its keep versus Guzzle:
use Swoole\Coroutine;
use Yurun\Util\HttpRequest;
Coroutine\run(function () {
$results = [];
foreach ($urls as $i => $url) {
Coroutine\create(function () use ($url, $i, &$results) {
// one client per coroutine
$results[$i] = (new HttpRequest)->timeout(10)->get($url)->json();
});
}
});
Queued jobs: create the client inside handle(), not as a constructor-injected property on a long-lived job class, so retries and worker reuse start clean.
Cookie-aware flows (login then scrape): keep one HttpRequest instance across the sequence and use its cookie manager; the jar persists per instance.
Retry/timeout policy: put ->timeout() and ->retry() in the container binding so every caller inherits sane limits rather than relying on memory.
getStatusCode() yourself — forgetting this is the classic silent-bug source, and errors get cached as if they were successful payloads.Http::fake() equivalent. There is no test double, no recorded-transport hook, and no middleware pipeline. Inject a your-own interface and fake that in tests, or point a configurable base URL at a local stub server.Authorization header to user B's request. Bind as bind, not singleton, and never share a client across coroutines.Co\run fails or silently falls back. Guard the handler selection on extension_loaded('swoole') and on actually being in a coroutine.->retry() is not idempotency-aware — never let it retry a POST that charges a card, or add an idempotency key upstream.getHeaderLine('Location') semantics if you need the intermediate response, and cap max redirects for hostile upstreams.body() into a variable, especially in a worker that serves many requests.CURLFile objects, and the Swoole handler's accepted form differs from cURL's. Test uploads against both handlers if you switch between FPM and Octane.getStatusCode(), headers(), and the error fields on the response object before the body — a truncated body usually means a transport error, not a parsing problem. Verbose cURL output is only reachable through the pass-through options array, so log status + timing from your wrapper instead.HttpRequest or (better) decorate your own wrapper. Custom handlers are possible but are a big lift for a one-off need — reach for Guzzle if you need a middleware ecosystem.How can I help you explore Laravel packages today?