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

Remote Model Laravel Package

torann/remote-model

torann/remote-model brings Eloquent-style remote models to Laravel, letting your app query, create, update, and delete RESTful API resources over HTTP as if they were local database models, with support for scopes, relationships, and caching.

View on GitHub
Deep Wiki
Context7

Getting Started

Pick your adoption mode before you touch Composer. The last release is 0.2.1 (Jan 2017) and targets Laravel 5.x, PHP 7.0–7.1, and an old Guzzle major. On Laravel 10/11 with PHP 8.2+ you will almost certainly fail dependency resolution. Two realistic paths:

  • Legacy Laravel 5.x app: install and pin the release.
  • Modern stack: vendor the source into your repo (e.g. app/Remote/) and port the model + transport classes onto Laravel's Http client or Guzzle 7. Treat this as a 1–2 sprint fork with tests, not a composer require.

1. Install (legacy path).

composer require torann/remote-model:^0.2.1

Pin ^0.2.1 specifically — it contains the pagination fix, and pagination is one of the few features you'll actually lean on day to day.

2. Register the service provider. The package predates auto-discovery, so add it manually in config/app.php:

'providers' => [
    // ...
    Torann\RemoteModel\RemoteModelServiceProvider::class,
],

If you port to a newer app, register the same class from your own service provider.

3. Publish and point the config at your API.

php artisan vendor:publish --provider="Torann\RemoteModel\RemoteModelServiceProvider"

This produces config/remote-model.php holding the remote host/port/prefix and global headers. Look here first whenever a request hits the wrong host, a path prefix doubles up, or auth headers go missing — this file causes most of what looks like "package bugs."

4. Define your first model.

namespace App\Remote;

use Torann\RemoteModel\Model;

class User extends Model
{
    protected $endpoint = 'users';   // appended to the configured base URL
    protected $primaryKey = 'id';    // must match the key in the remote payload
}

$endpoint is what maps a Laravel model onto a remote route. Get it right and the rest of the config usually falls into place.

5. Start with a read-only, low-risk use case. An admin dashboard looking up a customer from a CRM, or reference data from an internal service. The canonical first call is a lookup:

$user = App\Remote\User::find(42);

if ($user) {
    echo $user->email;   // hydrated from the remote JSON payload
}

If that works end to end — correct URL, correct auth, attributes on the model — you've validated the whole path. Everything harder (writes, caching, pagination, error handling) is scope you add on top.

Where to look when things break, in order: config/remote-model.php → the model's $endpoint → the remote API's raw response → the outgoing HTTP request (log the URL, headers, body).


Implementation Patterns

One model class per remote resource, and keep it thin. Don't build a domain layer on the model — the model is the data-access layer; domain code sits above it.

class Invoice extends Model
{
    protected $endpoint = 'invoices';
    protected $primaryKey = 'invoice_id';
}

Read patterns you can rely on. find($id), all(), basic where(...), and pagination are the supported surface. Use them for record-shaped lookups and simple filters. For anything query-heavy (aggregations, multi-field sorting, deep server-side filtering), check what the remote API actually exposes first — the package can only forward what's there.

Pagination — stay on 0.2.1.

$page = Invoice::paginate(50);

Pagination was buggy before 0.2.1 and could silently truncate result sets. Pin the version and add a test that asserts the full result set plus correct page metadata.

Treat writes as a last resort. save() and delete() exist on the interface, but there are no transactions, no consistency guarantees, and a failed HTTP call surfaces as an exception mid-flow. If you must write:

  • Stick to single-record CRUD against a stable API.
  • Wrap each write in explicit try/catch and treat partial failure as a real state problem.
  • Never build a multi-step workflow that depends on two remote writes both succeeding.

Wrap the model behind an internal interface. This is the single highest-leverage pattern — it lets you swap the implementation later without touching call sites, and it's what makes a fork survivable.

interface CustomerRepository {
    public function find(int $id): ?CustomerData;
}

class RemoteCustomerRepository implements CustomerRepository {
    public function find(int $id): ?CustomerData {
        $raw = App\Remote\User::find($id);
        return $raw ? CustomerData::fromRemote($raw) : null;
    }
}

Domain code depends on CustomerRepository, never on App\Remote\User directly. When the package needs forking or replacing, the blast radius is one class.

Layer caching in a decorator, not in the model. The bundled caching isn't production-grade for multi-instance deployments (stampede and cross-pod consistency aren't solved). Wrap your repository in a cache decorator backed by Redis/memcached with a stampede lock, keyed on remote resource + primary key.

class CachedCustomerRepository implements CustomerRepository {
    public function find(int $id): ?CustomerData {
        return Cache::lock("customer:$id", 10)->block(5, fn () =>
            Cache::remember("customer:$id", 300, fn () =>
                $this->inner->find($id)
            )
        );
    }
}

Auth via config headers, not per call. Put tokens/API keys in config/remote-model.php so they're set once and rotate centrally. If the token expires, add a handler that refreshes and retries — the package won't do it for you.

Test against fixtures, not live services. Record representative API responses, feed them into the transport layer (mock the HTTP client, or substitute Http::fake() after a port), and assert attribute hydration, pagination shape, and error paths. Add contract tests — if the remote schema drifts you want a red test, not a silent null.

Migration-bridge pattern — the highest-value use case. Point a remote model at the system you're replacing, put an internal interface in front of it, and let call sites read through the interface while you swap in the new implementation later. This is where the package genuinely earns its keep: a temporary seam with a defined sunset date.


Gotchas and Tips

The Eloquent illusion is the real trap. The API looks like Eloquent, which invites assumptions it can't honor:

  • No relationships / lazy loading. $user->invoices won't behave as it does locally.
  • No real query scopes. where() forwards to the remote API; there's no local query builder translating arbitrary expressions.
  • No transactions. No rollback across remote writes.
  • No persistence. It's an HTTP call wearing an ORM costume.

Onboarding must lead with these limits, or junior devs will write Eloquent-shaped code that fails at runtime.

Version and compat decay. Old Guzzle major, PHP 7.0-era syntax, Laravel 5.x provider and contract signatures. On modern PHP/Laravel expect to either override Composer constraints (risky, unverified) or fork. Budget for Guzzle 7 / Laravel 10–11 contracts and a real test suite if you fork.

Silent attribute drift on upstream schema changes. Because hydration is dynamic, an upstream field rename surfaces as null on your model, not a type error. This is the most dangerous production failure mode. Mitigate with validation at the hydration boundary and contract tests against recorded fixtures.

Pagination bug — pin ^0.2.1. Anything older can silently truncate results. Even on 0.2.1, assert the expected count and page metadata in a test.

Config quirks. Host + prefix joining is the usual suspect — a trailing slash on the base URL can double up or drop a path segment. If requests 404 against an endpoint that plainly exists, dump the full URL being built before changing anything else.

No retries, backoff, rate limiting, or circuit breaking. Every remote failure becomes a hard exception in your request path. Add:

  • Retry with exponential backoff on transient errors (5xx, timeouts).
  • A circuit breaker so a slow remote doesn't take you down.
  • Explicit handling of auth token expiry.
  • A defined degradation path per call site (cached value, empty result, user-facing fallback).

Cache is naive by default. Local caching in a multi-pod deployment gives inconsistent reads and stampede-on-expiry. Use a shared store with a lock, or don't cache in the package layer at all — cache above the repository interface.

Error handling is your responsibility. Wrap find()/all()/save() in try/catch that distinguishes 404 (legitimately absent record → return null) from transport/timeout errors (surface or degrade explicitly). Don't let a 500 from the remote look like "record not found."

Debugging tips:

  • Enable request/response logging behind a local debug flag to see the exact URL, headers, and body — most "package bugs" are config or auth.
  • Reproduce failures with a single find() call before debugging scopes or pagination.
  • Keep a small fixture set in the repo and test the model against it in CI — that's your only reliable regression net given 0 dependents and no upstream tests.

Extension points to know:

  • Subclass Torann\RemoteModel\Model and override per-model config ($endpoint, $primaryKey, headers).
  • Swap or extend the transport (HTTP client / handler stack) to add retries, logging, tracing.
  • Override the caching layer, or bypass it entirely and cache above the repository interface.
  • Wrap everything behind an interface — that's the extension point that survives a fork.

Licensing and support. BSD-2-Clause is permissive and legally low-risk, so the barrier is decay, not licensing. There's no maintainer, no community, and 0 dependents — every future bug fix, framework bump, and security patch is yours. Treat the package as an unmaintained reference implementation: adopt the pattern, vendor the code if you keep it, and assign an internal owner before it reaches any critical path.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons