victorycodedev/nativephp-fetch
Fetch-style HTTP client for NativePHP, giving native desktop apps a simple, familiar way to make requests. Lightweight and Laravel-friendly, it aims to streamline pulling remote data into your native windows without leaving the ecosystem you already know.
NativePHP Fetch is the HTTP client you reach for whenever your native app needs to talk to a remote API — it mirrors Laravel's Http facade closely enough that you already know 90% of it, but it adds native-friendly lifecycle events, offline/caching behaviour, and #[On] component integration.
Minimal steps
composer require nativephp/fetch
php artisan vendor:publish --tag=nativephp-fetch-config
Look at three places first:
config/nativephp-fetch.php — timeouts, base URL, caching, retry defaults.Fetch facade (use Native\Fetch\Facades\Fetch; — confirm the exact namespace with php artisan about or the package's composer.json autoload, it differs between the standalone and bundled Mobile installs).FetchRequestStarted / FetchRequestCompleted / FetchRequestFailed event classes — these are the cheapest observability you'll ever get.First use case — one request inside a NativeComponent:
use Native\Fetch\Facades\Fetch;
use Native\Fetch\Events\FetchRequestFailed;
Fetch::baseUrl('https://api.myapp.test')
->acceptJson()
->timeout(10)
->get('/profile')
->throw();
If you're wiring this into a screen, start with the global event listeners rather than sprinkling try/catch everywhere — a single FetchRequestFailed listener that pushes a toast is usually the whole error UX for a v1.
Macros for your authenticated client. This is the headline feature and it should replace every copy-pasted header block in your codebase. Register macros in a service provider's boot():
use Native\Fetch\Facades\Fetch;
public function boot(): void
{
Fetch::macro('api', function () {
return Fetch::baseUrl(config('services.api.url'))
->withToken(Cache::get('api_token'))
->acceptJson()
->timeout(15)
->retry(2, 200);
});
}
Then anywhere: Fetch::api()->get('/orders')->collect(). Because macros return a configured pending request, you can still chain per-call overrides (Fetch::api()->timeout(45)->post(...)).
Base URLs + relative paths. Set baseUrl once (config or macro) and use relative paths everywhere, including query strings — the release explicitly covers query parameters and downloads:
Fetch::baseUrl('https://cdn.myapp.test')->download('/reports/2024-05.pdf', $path);
Fetch::api()->get('/search', ['q' => 'invoice', 'page' => 2]);
Prefer a named macro over a bare Fetch::baseUrl() call at every call site; the relative-path mental model only pays off if the base is consistent.
Two-layer event handling. The v1.1.0 split is genuinely useful — use each layer for what it's good at:
Event::listen / listener classes) for cross-cutting concerns: logging, analytics, a global loading spinner counter, token-refresh-on-401.#[On] in NativeComponents for local UI state: $this->loading = false, $this->error = $e->message.A global request counter is a nice pattern for a shared progress indicator:
Event::listen(FetchRequestStarted::class, fn () => Inertia::share('fetching', true));
Event::listen(FetchRequestCompleted::class, fn () => Inertia::share('fetching', false));
Status helpers instead of magic numbers. With statusIs() and the expanded helper set, error branches read like English:
$response = Fetch::api()->post('/orders', $payload);
$response->created(); // 201
$response->unprocessableEntity(); // 422 — validation
$response->unauthorized(); // 401 — refresh token
$response->tooManyRequests(); // 429 — back off
$response->statusIs(409); // no named helper yet, still readable
The 422 branch is the one worth standardising: return it straight to your form component and map $response->json('errors') to field errors.
Testing. Keep using Fetch::fake() — the release is explicitly backward compatible, and macros/base URLs work with fakes. Assert against the same fluent helpers so tests and production share vocabulary:
Fetch::fake(['*' => Fetch::response(['id' => 1], 201)]);
Fetch::api()->post('/orders', $payload)->assertCreated();
https://... URL to a pending request that has a baseUrl, you're relying on the client's URL-joining rules. Don't mix — pick relative paths for the macro'd client, and spin up a second macro/client for third-party endpoints. Watch trailing slashes on the base (/api vs /api/) — the classic cause of //users.if (! Fetch::hasMacro('api')) or keep registration strictly in boot().FetchRequestFailed listener shows a toast and your component catches the exception and sets $error, the user sees it twice. Pick one owner per concern: component owns the message, global listener owns the log/telemetry.FetchRequestCompleted — for response inspection use a ->then()/->tap() on the request or the #[On] handler that actually has the response.Authorization before it hits disk.->throw() vs helpers. Mixing throw() with $response->notFound() checks is contradictory: throw() raises before you ever see the response object. Use throw() at the edges (background sync), helpers in UI-facing paths where you want branching.timeout(15) silently overrides nothing but will surprise you when a large download times out — override per call for download()/post() with big payloads, and set a longer retry backoff for downloads.retry() in a base macro means every POST gets retried. That's a duplicate-order bug waiting to happen. Either keep retry out of the shared macro or make a Fetch::apiSafe() variant for idempotent GETs only.baseUrl set before the verb? (2) does the URL resolve as expected — log it in FetchRequestStarted; (3) is a Fetch::fake() still active from a previous test or a stray call; (4) is the failure being swallowed by a global listener that never re-throws.FetchRequestFailed that checks statusIs(401), refreshes the token, and retries the request is the natural place to centralise auth — much cleaner than wrapping every call site, and it's the pattern most likely to survive future releases.How can I help you explore Laravel packages today?