Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Saber Laravel Package

swlib/saber

Saber is a modern HTTP client for PHP with a clean, chainable API. It streamlines API requests by handling headers, query parameters, JSON bodies, authentication, and response parsing in readable code, so integrations stay concise and maintainable.

Deep Wiki
Context7

Getting Started

Saber is a Swoole-coroutine HTTP client that gives you a Guzzle-ish, PSR-7-based API without the blocking I/O. Because it depends on Swoole's coroutine scheduler, it only works when executed inside a coroutine context — a Swoole\Coroutine\run() block, a Swoole HTTP/TCP worker, or any code already running under Co\run().

Minimum setup:

composer require swlib/saber
# requires ext-swoole (4.4+ recommended) with coroutine support

Your first request:

use Saber\Saber;

Co\run(function () {
    $response = Saber::get('https://httpbin.org/get');

    echo $response->getStatusCode();           // int
    echo $response->getBody()->getContents(); // PSR-7 stream
});

Where to look first:

  • README.md for the canonical Saber::get/post/... + Saber::wait() examples.
  • src/Saber.php (the client) and src/Coroutine.php (the concurrency plumbing) — these define the public surface you will actually call.
  • The adapters directory (src/Adapter/...) — the Swoole adapter is what turns PSR-7 requests into Swoole\Coroutine\Http\Client calls; a second adapter exists for testing/fallback.
  • examples/ and tests/ for working snippets of concurrency, interceptors, and exception handling.

First real use case: fanning out to several internal microservices (or third-party APIs) from a single Swoole worker without blocking — replace a chain of file_get_contents() / blocking Guzzle calls with concurrent Saber requests. Start with one request to confirm the coroutine wiring (you'll get a hard error if you're not in a coroutine), then move to the concurrent pattern below.

Implementation Patterns

Single request inside a worker/controller. Saber calls yield the coroutine, so they compose naturally with other Swoole I/O (DB, Redis, filesystem). This is the drop-in replacement for blocking HTTP:

Co\run(function () {
    $resp = Saber::get('https://api.internal/users/42', [
        'timeout'         => 2,
        'connect_timeout' => 0.5,
        'headers'         => ['Authorization' => 'Bearer ' . $token],
    ]);

    if ($resp->getStatusCode() !== 200) { /* ... */ }
    $data = json_decode((string) $resp->getBody(), true);
});

Configured client instance. For anything non-trivial, create a client with a base URI and shared defaults, rather than passing options on every call:

$api = Saber::create([
    'base_uri' => 'https://api.internal',
    'timeout'  => 3,
    'headers'  => ['X-Service' => 'billing'],
]);

$users = $api->get('/users/42');
$orders = $api->get('/orders?user=42');

Concurrent fan-out — the pattern you'll use most. The idiomatic Saber concurrency shape is: issue several requests on the same client, then call Saber::wait() to execute them together in one coroutine wave:

Co\run(function () {
    $saber = Saber::create(['base_uri' => 'https://httpbin.org']);

    $saber->get('/get');
    $saber->get('/delay/1');
    $saber->get('/uuid');

    $responses = Saber::wait();          // array of PSR-7 responses
    foreach ($responses as $response) {
        echo $response->getStatusCode(), PHP_EOL;
    }
});

Use this for API aggregation, dashboards, or "fetch N resources for one page render" endpoints — it's where Saber pays for itself versus sequential Guzzle calls.

Middleware / interceptors for cross-cutting concerns. Attach auth headers, tracing IDs, retry logic, and logging through interceptors so route code stays clean:

$saber = Saber::create(['base_uri' => 'https://api.internal']);
$saber->intercept(function ($request, callable $next) {
    $request = $request->withHeader('X-Request-Id', bin2hex(random_bytes(8)));
    return $next($request);
});

Interceptors are also the cleanest place to implement retries (inspect status/exception, sleep with Co::sleep(), and call $next() again) and JSON decoding, so callers get typed results.

Integration with PSR-7/PSR-18-style code. Responses are PSR-7 ResponseInterface objects, so (string) $response->getBody(), $response->getHeaderLine() etc. work as usual, and you can hand responses to existing middleware, loggers, or serializer code.

Retry + backoff helper. Since you're inside a coroutine, Co::sleep() gives non-blocking backoff:

$saber->intercept(function ($request, callable $next) {
    $attempt = 0;
    beginning:
    try {
        $response = $next($request);
        if ($response->getStatusCode() >= 500 && $attempt < 3) { throw new \RuntimeException('retry'); }
        return $response;
    } catch (\Throwable $e) {
        if (++$attempt >= 3) throw $e;
        \Swoole\Coroutine::sleep(0.2 * $attempt);
        goto beginning; // or a loop
    }
});

(Write that with a while loop in real code; the point is that the backoff doesn't block other coroutines.)

Testing/CI. Because Saber needs a coroutine runtime, wrap tests in Co\run(), or configure the non-Swoole adapter if you truly need to run without coroutine support (it degrades to synchronous behavior — useful for a smoke test but defeats the purpose).

Gotchas and Tips

  • "API must be called in the coroutine." This is the #1 first-day error. Every Saber::get/post/... call, and Saber::wait(), must be inside a coroutine. In a plain FPM process it will fail; you need a Swoole server worker or Co\run().
  • Saber::wait() is a global-ish batching step, not a generic promise. The batching is per-client/coroutine; mixing clients, or calling wait() from a different coroutine than the one that enqueued the requests, won't do what you expect. Keep the "enqueue several, then wait" block together.
  • Response bodies are streams, not strings. $response->getBody() returns a PSR-7 stream. If you echo it, cast to (string) or call ->getContents()`. Reading a stream twice returns empty — cache the decoded value if you need it more than once.
  • Large downloads are a memory risk. Saber buffers response bodies; it is not a streaming downloader. For big files use Swoole\Coroutine\Http\Client directly with a write handler, or stream to disk at the socket level.
  • Timeouts are per-attempt, not a deadline. timeout and connect_timeout govern a single HTTP exchange. If you add redirects, retries, and DNS retries, the wall-clock time can be multiples of timeout. Wrap the whole logical operation in Swoole\Coroutine::withTimeout() if you need a hard deadline.
  • Concurrency limits are Swoole's, not Saber's. The number of simultaneous connections is bounded by the worker's settings (and the OS). For a fan-out of 100 calls, cap it yourself (batches or a Channel-based semaphore) — otherwise you can exhaust file descriptors or the target's rate limit.
  • Cookies and redirects aren't automatic the way Guzzle users assume. Configure explicit cookie storage/allow_redirects options on the client if you need session behavior. Many "why am I getting 302 with an empty body" bugs are just redirects not being followed.
  • TLS/SNI and proxies fail quietly. Verify ssl_cert/verification and proxy settings against the Swoole version — different Swoole minors handle SNI differently. Test against a real HTTPS endpoint, not just localhost.
  • Error handling contract. Failed requests surface as exceptions (connect vs. request-level), and status-code-only errors may not throw unless configured. Decide per client whether you want exceptions or manual status checks — mixing both across a codebase makes bug triage painful.
  • Release age / maintainability. Last release is April 2021. Pin the version in composer.json, and treat it as a known-good dependency rather than one you upgrade freely. For new projects in 2024+, also evaluate Hyperf's Guzzle coroutine adapter, Symfony HttpClient with a coroutine-based dispatcher, or raw Swoole\Coroutine\Http\Client — Saber's ergonomics (PSR-7 + interceptors + wait()) are still nice, but you should consciously accept the maintenance risk.
  • Debugging coroutine stack traces. Swoole's stack traces are shallower than PHP's normal ones. Add an interceptor that logs method, URI, status, and elapsed time — it's the fastest way to see what's actually hanging versus queued.
  • Extension points worth knowing. The adapter is configurable (swap in a fake for tests), interceptors are the right hook for auth/retry/telemetry, and the exception handler / exception callable option lets you centralize failure handling. Reuse configured client instances per service rather than recreating them per request — that keeps default headers and connection state consistent.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo