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.
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:
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).
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:
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.
The Eloquent illusion is the real trap. The API looks like Eloquent, which invites assumptions it can't honor:
$user->invoices won't behave as it does locally.where() forwards to the remote API; there's no local query builder translating arbitrary expressions.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:
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:
find() call before debugging scopes or pagination.Extension points to know:
Torann\RemoteModel\Model and override per-model config ($endpoint, $primaryKey, headers).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.
How can I help you explore Laravel packages today?