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 Caching Laravel Package

mikebronner/laravel-model-caching

Automatically caches Eloquent model and relationship queries, then invalidates them when data changes—no manual cache keys. Add a trait to your models to cut database load on read-heavy pages with self-invalidating, Laravel-native caching.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and enable, in three steps:

composer require mike-bronner/laravel-model-caching

The service provider is auto-discovered — there is nothing to register. Then add the trait to a base model so every model inherits it:

<?php

namespace App\Models;

use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Model;

abstract class BaseModel extends Model
{
    use Cachable;
}

(Alternative: extend GeneaLabs\LaravelModelCaching\CachedModel directly. Only do that on new models — switching an existing model's parent class is a bigger diff than adding a trait.)

Then publish the config once, so you can see and tune the knobs:

php artisan modelCache:publish --config

Where to look first:

  1. config/laravel-model-caching.php — five settings, all of them matter (enabled, store, cache-prefix, use-database-keying, fallback-to-database).
  2. config/cache.php — the package's biggest practical constraint is the driver. Redis, Memcached, APC and DynamoDB are supported; array, file and database are not. If your CACHE_STORE is file (the Laravel default in many installs), the package silently gives you nothing useful. Set a dedicated store:
MODEL_CACHE_STORE=model-cache
// config/cache.php
'stores' => [
    'model-cache' => [
        'driver' => 'redis',
        'connection' => 'cache',
    ],
],
  1. Your models — confirm the trait resolves (class_uses_recursive(Post::class) or just check behaviour).

First use case: take your heaviest read-only index page — a paginated listing with one or two with() eager loads — point the trait at its model, and measure. This is where the package earns its keep:

$posts = Post::with('comments', 'tags')
    ->where('published', true)
    ->latest()
    ->paginate(15);

No cache key, no TTL, no Cache::forget() in an observer. Creating a Comment invalidates the Post and Comment query caches automatically. Verify the win with Laravel Telescope / Debugbar query count on the second request, then hit "create comment" and confirm the next read is a miss followed by a fresh, correct result. That single loop — cached read, write, invalidate, re-read — is the whole mental model of this package.

Implementation Patterns

Trait on a base model, not per-model. One BaseModel with use Cachable keeps the decision auditable in one place, and lets you exclude a model deliberately (class Post extends Model instead of BaseModel) rather than accidentally. Reach for a $cachePrefix or disableCache() on models you never want cached.

Always eager-load what you render. Only with() relationships are cached. Lazy-loading inside a Blade loop is both an N+1 and uncached, on every request:

// cached: query + comments + tags
$posts = Post::with('comments', 'tags')->where('published', true)->get();

// not cached, plus N+1
$posts = Post::where('published', true)->get();
foreach ($posts as $post) { $post->comments; }

Treat this as an architectural rule: if a relation appears in a response, it goes in with().

Bypass deliberately, in seeders and imports. Reseeding through a cached model will hand you stale rows, and every insert will churn the cache. Scope the bypass rather than flipping the global env:

use GeneaLabs\LaravelModelCaching\Facades\ModelCache;

ModelCache::runDisabled(function () {
    $this->seed(PostSeeder::class);
});

// or one query chain only
Post::disableCache()->where('legacy', true)->get();

disableCache() affects that chain only — the next query is cached again. Don't assume it sticks.

Isolate tenants with prefixes. For multi-tenant apps, either set the global config value or put it on the model. The per-model property is the one that scales to dynamic tenants:

class Post extends BaseModel
{
    protected $cachePrefix = 'tenant-123';
}

Pair this with MODEL_CACHE_USE_DATABASE_KEYING=true (the default) if tenants also have separate databases — cache keys then include connection and database name, so you cannot serve tenant A's rows to tenant B.

Cool-down for high-churn writes. For a model written constantly (comments, page views, counters), every write flushing the cache can make the cache useless. Two steps, and the second is easy to forget:

class Comment extends BaseModel
{
    protected $cacheCooldownSeconds = 300; // declares the default only
}

// this is what actually activates it
Comment::withCacheCooldownSeconds()->get();
Comment::withCacheCooldownSeconds(30)->get(); // per-query override

During the window, writes don't flush; after it expires, the next write flushes and re-warms. Accept the bounded staleness as an explicit product decision, per model.

Add invalidation to your deploy and ops runbook.

php artisan modelCache:clear

You'll want this after bulk imports, after raw SQL data fixes, and when investigating "why is it showing old data". Note on DynamoDB that this rotates a namespace key — it makes rows unreachable, it does not delete them; TTL does that later.

Wrap untrusted cache backends in fallback. If Redis going down should degrade to slow-but-working rather than 500s:

MODEL_CACHE_FALLBACK_TO_DB=true

Cost is a warning log per request plus full DB load — fine as a safety net, not as a capacity plan.

Gotchas and Tips

A set of query shapes silently skips the cache. Know them before you benchmark and conclude "it doesn't help":

  • select(...) — custom column selections bypass caching entirely.
  • Lazy-loaded relations — only with() is cached.
  • inRandomOrder() — caching disabled by design.
  • Queries inside transactions — the cache is not flushed on commit; call flushCache() yourself after committing.

Raw query-builder writes do not invalidate. DB::table('posts')->update(...), mass import scripts, and anything bypassing Eloquent events leave stale cache behind. This is the most common production surprise. Discipline: writes go through Eloquent, or you follow them with modelCache:clear / flushCache().

The User model works but deserves caution. The trait doesn't conflict with auth, but avoid cool-down on User (stale permissions / profile data), and keep user mutations on Eloquent so invalidation fires. If you cache User and also authenticate against it, test login and permission changes explicitly.

Check your driver first when "nothing happens". array, file and database stores are unsupported. A fresh Laravel app on CACHE_STORE=file will look like the package is broken. Verify before debugging anything else:

config('cache.default');           // must be redis/memcached/apc/dynamodb
config('laravel-model-caching');  // enabled? store set correctly?

Global kill switch for bisecting. MODEL_CACHE_ENABLED=false is the fastest way to prove whether a bug is cache-related. Keep it as an env var you can flip in a staging environment, not a permanent config edit.

Upgrades can cold-start your cache. Cache key and tag formats are the package's contract with consuming apps — a version bump can change them, so every cached query reads cold after upgrading, and any manual Cache::forget() calls keyed to old formats become no-ops. Plan upgrades for low-traffic windows and watch DB load right after deploy.

DynamoDB behaves differently from Redis, and you must plan for it. Invalidation is logical namespace versioning, not tag deletion:

  • Old rows linger until TTL. Enable TTL on expires_at or the table grows unbounded on write-heavy models.
  • modelCache:clear won't shrink the table — that's expected, not a bug.
  • Multi-tag invalidation isn't atomic; a crash mid-invalidation can leave partial rotation that a later invalidation converges.
  • Frequent invalidations increase table size. If you see steady growth, that's the signal to move to Redis for that workload.

The cool-down trap. Setting $cacheCooldownSeconds on the model does nothing on its own. If writes still flush immediately, you forgot withCacheCooldownSeconds() somewhere in the read path. Conversely, if data is stale far longer than expected, grep for withCacheCooldownSeconds — someone activated it and nobody documented it.

Pagination is cached including its page. That's usually what you want, but a deep-link ?page=97 miss on a rarely visited page will still materialise a cache entry. If your page-count is huge, consider simplePaginate() to cut the count variants — or accept the cardinality and monitor cache memory.

Don't cache what changes per request. Anything keyed on auth()->id(), a session filter, or a "now"-relative window is a correctness bug waiting to happen, because the package's key derives from the query, not from request context that isn't in the SQL. If a filter lives in PHP rather than the where clauses, exclude the model or disableCache() that chain.

Debugging stale data, in order: (1) modelCache:clear — does it fix it? (2) Was the write done via Eloquent? (3) Is a cool-down active on that model? (4) Is a select() or inRandomOrder() silently bypassing? (5) Is the cache prefix / DB keying splitting you across two entries you thought were one?

Extension point worth knowing: the package registers app('model-cache')->runDisabled(...), and this is the clean seam for wrapping test setup, importers, and queue jobs that rewrite large datasets — preferable to scattering disableCache() calls, since it's one place to audit.

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.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections