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

Laravel Model Cache Laravel Package

ymigval/laravel-model-cache

Transparently caches Eloquent query results with zero syntax changes, auto-invalidating on create, update, delete, and restore. Configurable TTL, stores, and tags for read-heavy Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pick the right store first. The package tags every cache entry (['model_cache', ModelClass, table]), so the store you choose determines whether invalidation is surgical or scorched-earth:

composer require ymigval/laravel-model-cache
php artisan vendor:publish --provider="YMigVal\LaravelModelCache\ModelCacheServiceProvider" --tag="config"
CACHE_STORE=redis
MODEL_CACHE_STORE=redis   # optional: isolate model caching from app cache

config/model-cache.php is the only file worth reading on day one — four keys: cache_duration (minutes, default 60), cache_key_prefix, cache_store, enabled.

2. Add the trait to one read-heavy model, not all of them:

class Post extends Model
{
    use HasCachedQueries;

    protected $cacheMinutes = 30;   // per-model TTL
    protected $cachePrefix  = 'posts_';
}

3. First use case: the homepage/listing query you already know is hot. No code change required — the trait swaps newEloquentBuilder() for a CacheableBuilder, so Post::where('published', true)->orderByDesc('created_at')->take(10)->get() hits Redis on request #2. Watch it work with DB::listen() or Telescope; that's your fastest sanity check that the builder replacement actually took effect for this model.

Where to look first: config/model-cache.php → the trait on one model → storage/Redis key inspection → php artisan mcache:flush when you've made a mess.

Where to look second: vendor/ymigval/laravel-model-cache/src/CacheableBuilder.php. This package has ~78 stars, zero dependents, and a single maintainer; you will be reading its source when something behaves oddly. Budget an hour for that before shipping to production.

Implementation Patterns

Be deliberate about implicit vs. explicit. Both hit the same code path, so pick by intent, not performance:

// Implicit: inherited code, no diff, invisible to reviewers
$posts = Post::where('published', true)->get();

// Explicit: new code, self-documenting, greppable
$posts = Post::where('published', true)->getFromCache();
$post  = Post::where('slug', $slug)->firstFromCache();

Rule of thumb: use getFromCache()/firstFromCache() in anything you write from scratch, and let get()/first() handle legacy call sites. Then grep -r "getFromCache" gives you the full inventory of cached read paths — invaluable during an incident.

Bake TTLs into scopes, not controllers. This is where the package pays off:

public function scopePopular($query)
{
    return $query->where('views', '>', 1000)
                 ->orderByDesc('views')
                 ->remember(60);
}

Every caller of Post::popular() inherits correct caching, and there's exactly one place to tune the TTL. Same trick for "recently updated" widgets, navigation trees, and settings lookups.

Use remember() at the call site only for request-context variation — e.g. shorter TTLs for staff previewing content:

$minutes = $user->isAdmin() ? 5 : 60;
$posts = Post::latest()->remember($minutes)->get();

Note that this fragments your cache keys by role. Keep it to two or three tiers maximum.

Relations: cache the parent, not the graph. with() runs a separate builder for the related model, so caching behavior follows the trait on that model, not the parent:

// Comments are fresh each request if Comment lacks the trait
$posts = Post::with('comments')->getFromCache();

// Explicitly opt a relation out
$posts = Post::with(['comments' => fn ($q) => $q->withoutCache()])->getFromCache();

Prefer withoutCache() over not-eager-loading — you still want the single eager-load query, just not a cached copy that goes stale the moment someone comments. And regardless of caching, keep eager loading: a cache hit on posts doesn't stop an N+1 on relations.

Invalidate pivot tables with ModelRelationships. Model events fire on save/delete, but attach()/sync()/detach() write to the pivot table and don't touch the parent row — cached $post->tags results go stale:

class Post extends Model
{
    use HasCachedQueries, ModelRelationships;
}

$post->tags()->attach($tagId);                          // now flushed automatically
$post->syncRelationshipAndFlushCache('tags', [1, 2, 3]); // or be explicit

Add ModelRelationships to any model with belongsToMany that appears in a cached query. This is the most commonly missed step.

Wire flushing into deploy and import workflows. The moment you step outside Eloquent (DB::table(...)->update(), a CSV import, a queue job using raw SQL), events don't fire and cache goes stale invisibly:

php artisan mcache:flush "App\Models\Post"   # targeted, tag-based
php artisan mcache:flush                     # everything tagged model_cache

Put php artisan mcache:flush in your deploy script after migrate. Note that with file/array/database drivers this prompts for confirmation before clearing the whole cache — so in non-interactive contexts use --no-interaction only if you truly mean it, or make the call from application code instead:

Post::flushModelCache();   // whole class
$post->flushCache();       // single instance

Feature-flag it. Set enabled => env('MODEL_CACHE_ENABLED', true) so you can flip caching off in production without a deploy. Keep MODEL_CACHE_STORE pointed at a store separate from your session/queue cache so a full flush doesn't nuke unrelated data.

Gotchas and Tips

The tag requirement is not optional. Redis and Memcached give you real per-model invalidation. file, array, and database degrade to "flush everything on any write" — which on a busy app means your cache is empty most of the time and you've added overhead for nothing. If you can't run Redis, this package is probably the wrong tool.

Tests will behave differently than production. array is the default test driver, and it has no tag support, so every model save flushes the entire cache. Tests that depend on cache state will be flaky in ways production isn't (and vice versa). Set MODEL_CACHE_ENABLED=false in phpunit.xml for most suites, and write a small dedicated suite against Redis for the caching behavior itself.

Mass updates silently bypass invalidation. Post::where('status', 'draft')->update([...]) goes through the builder, not model instances — no updated event, no flush. Same for insert(), upsert(), DB::statement(), and anything wrapped in a transaction that rolls back after partial writes. Whenever you write this kind of code, follow it with an explicit Post::flushModelCache().

Cache keys derive from SQL + bindings, so trivially different queries are different keys. Post::find(1) and Post::where('id', 1)->first() may or may not collide; where('a',1)->where('b',2) and where('b',2)->where('a',1) will not. This isn't a correctness problem, but it means your hit rate is lower than you'd guess and your cache grows faster. Standardize query construction in scopes rather than trusting the key generator to dedupe.

Replacing the query builder is deep surgery. CacheableBuilder sits between your model and Illuminate\Database\Eloquent\Builder. Anything that type-hints or reflects on the concrete builder class — third-party packages (admin panels, audit logs, search tools), custom macros, cursor(), lazy(), chunkById() — may behave unexpectedly or throw. Roll this out one model at a time and exercise the full CRUD surface of that model, including the admin panel, before expanding.

remember() must precede the terminal call. Post::where(...)->get()->remember(30) is a Collection method — the query already ran uncached. Chain order matters, and nothing will warn you.

No stampede protection. When a hot key expires, every concurrent request hits the database simultaneously. For your highest-traffic endpoints, either use a long TTL plus explicit flushModelCache() on writes, or wrap the read in Cache::lock() yourself.

Plan for cold-cache deploys. A deploy that changes a cache key's shape (new cachePrefix, changed global scope, new default ordering) leaves orphans behind that never get read or invalidated. Run php artisan mcache:flush as part of every deploy — it's cheap insurance and takes a second.

Debug by looking at the keys. Inspect Redis with KEYS model_cache_* (or SCAN in production) to confirm what's actually cached and confirm a trait is really active on a model. If a model's keys never appear, the trait isn't on it, or enabled is false.

Vendor risk is real. One maintainer, zero dependents, and a package that monkey-patches a core Laravel class is a genuine upgrade hazard — Laravel 13 support today doesn't guarantee Laravel 14 tomorrow. Mitigations: pin to an exact minor (composer require ymigval/laravel-model-cache:1.2.*), isolate cached reads behind repository/scope methods so you can rip it out in one commit, and keep an eye on the changelog before each Laravel major upgrade. If the package ever stalls, Cache::remember() inside those same scopes gets you 80% of the benefit in an afternoon.

Extension points worth knowing: $cacheMinutes and $cachePrefix per model, overriding newEloquentBuilder() in your own base model to apply the trait globally, withoutCache() for targeted opt-outs, and config('model-cache.enabled') as a runtime kill switch.

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.
adachsoft/command-executor-lib
tagixo/document-builder
tagixo/page-builder
tagixo/mail-builder
tagixo/form-builder
primix/primix
tagixo/core
alesima/laravel-azure-service-bus
tomshaw/shopcart
likyly/laravel
darthsoup/shoppingcart
tdt/core
labyrinthe/connect-php
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite