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

Eloquent Redis Mirror Laravel Package

petkakahin/eloquent-redis-mirror

A Laravel package that mirrors Eloquent models into Redis, keeping cached copies in sync on save and delete so you can serve fast reads without touching the database. Drop-in setup that leaves your existing Eloquent workflow unchanged.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes)

composer require petkakahin/eloquent-redis-mirror
php artisan vendor:publish --tag=redis-mirror-config   # inspect/commit the config

Confirm the package's Redis connection is configured (separate from your cache/queue DB index is strongly recommended) and that phpredis/predis is available in the runtime that serves requests — not just in your local container.

First place to look: the published config file. It governs the connection name, key prefix, warmed-flag TTL (24h default), and fallback behaviour. Set the prefix per environment before you mirror anything, because changing it later orphans keys.

First use case: pick exactly one read-heavy model behind a hot endpoint — a Project shown on a dashboard, a catalogue Product, a User profile — and:

  1. use HasRedisCache; on the model.
  2. Declare protected array $redisRelations = ['categories', 'tags']; for relations you actually eager-load on that endpoint.
  3. Ship it unchanged. The Eloquent API is identical; Redis fills lazily on the first read.

Verify it's working with redis-cli -n <db> --scan --pattern '<prefix>*' and MONITOR while hitting the endpoint once cold and once warm. Compare response time against the pre-change baseline before rolling to more models.

Do not start by adding the trait to every model. Add it where reads dominate and where you can tolerate a cache-shaped failure mode.

Implementation Patterns

Keep all writes in Eloquent. The synchronisation contract is model events (created/updated/deleted/restored). Anything that goes through a model instance is safe:

$project->update(['name' => 'New']);
$project->tags()->attach([5, 8]);      // pivot keys updated
$project->delete();                     // soft delete honoured
$project->restore();

Read paths stay boring — that's the point. No new API to learn, so existing controllers, API Resources, and Blade views need zero changes:

Project::find(7);
Project::with('categories.tasks')->find(7);   // served from Redis
Project::where('status', 'active')->paginate(25);
$project->categories()->exists();

Custom/third-party relations need the explicit mapping plus a context call. This is the one place the abstraction leaks, so wrap it in a repository method rather than scattering it:

protected array $redisCustomRelations = [
    'sortedItems' => BelongsToSortedMany::class,
];

// direct call on the relation must go through the context helper
$project->withRedisContext()->sortedItems()->get();

Warm deliberately, not accidentally. Cold start is lazy and per-key, which means the first request after a flush or a fresh deploy pays the DB cost (and a thundering herd if traffic is high). Add a warm-up job to your deploy pipeline or a scheduled command that touches the top N hot records per mirrored model — run it after migrations and before the load balancer flips.

Treat the trait as a read-optimisation layer, not a data layer. Structure services so Redis-mirrored reads sit behind the same repository interface as before. When a query shape isn't mirrored, it falls back to the DB silently — your code shouldn't care, but your dashboards should.

Batch operations are wrapped in MULTI/EXEC, so a create with several relations doesn't leave half-written keys. Lean on that when writing importers/seeders that go through models — but see the gotchas about DB::table below.

Testing: flush the mirror namespace in setUp (or a trait alongside RefreshDatabase), and keep a small regression suite that asserts a write through Eloquent is immediately visible via find(). If your CI has no Redis, verify the fallback path is exercised — that's the path that runs in production incidents.

Gotchas and Tips

Mass updates bypass model events — this is the big one. Project::where(...)->update([...]), DB::table('projects')->update(...), raw SQL, truncate(), and DB-level ON DELETE CASCADE will not touch Redis. Result: stale reads that look like ghosts. Audit for these before enabling the trait, and route them through chunked model updates or add an explicit invalidation step.

Transactions can leave Redis ahead of the database. Writes inside DB::transaction() fire model events before commit; a rollback leaves mirrored keys describing rows that never existed. Prefer dispatching writes after commit (afterCommit, or a queued job that performs the model write) for anything that can fail mid-transaction.

Silent fallback is a feature and a trap. When Redis is down everything still works — against the database, at database speed. Without a log/metric you won't notice you're degraded. Add an alert on the fallback path and watch Redis availability as a first-class dependency, not a nice-to-have.

Shared Redis, colliding keys. Queue workers, Horizon, sessions, and the mirror may all hit the same Redis. Use a dedicated DB index and a unique prefix per app and per environment. Two staging environments sharing a prefix is a genuinely confusing afternoon.

Memory and eviction. The mirror stores serialised models (and separate pivot keys). Don't mirror models with large text/blob columns. Set maxmemory-policy to an eviction policy that tolerates mirror keys disappearing (they'll re-warm) — but be aware allkeys-lru will also evict your sessions and queue data.

Serialisation round-trips. Casts, custom accessors, date formats, and non-serialisable properties need checking: assert that a mirrored model is == to a freshly hydrated one for the fields you care about, especially for enums and encrypted casts.

Query shapes are not all mirrored. find, findMany, with, first, paginate, exists are covered; aggregates, whereHas across unmirrored relations, and arbitrary orderBy on non-indexed columns will fall back. Confirm on your actual hot queries rather than assuming, and don't refactor a query to "make it cacheable" if the DB plan was already fine.

Relation/config drift needs a re-warm. Adding or removing entries in $redisRelations, or changing key-prefix/schema, requires flushing the mirror namespace and warming again. Make that a documented step in the upgrade runbook; otherwise you'll debug inconsistency that's really just old keys.

Soft deletes and cascades. delete()/restore() are handled, but verify child keys are cleaned when a parent is force-deleted, and check any FK cascade at the DB level that fires outside Eloquent.

Extension points worth knowing: the custom-relation map, the context helper, the key/prefix builder, and the model events themselves. If you need bespoke invalidation (e.g. a nightly bulk job), hook the same events or call the invalidation explicitly rather than reaching into Redis directly.

Debugging checklist: MONITOR on the mirror connection, scan the prefix for the expected keys, confirm the warmed flag exists, then check whether your write path actually fired a model event. Nine times out of ten the bug is a write that never touched a model.

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.
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
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console