laravel-model-caching maintained by ghostcompiler
Laravel Model Caching
Relationship-aware Eloquent model caching for Laravel.
This package is designed for applications where cached parent queries must be invalidated when loaded child models change. It is not only a query cache. It stores deterministic query results and records a lightweight dependency index from model instances to cache keys.
Features
- Opt-in model caching through
HasModelCaching remember(),rememberForever(),dontCache(), and optionalauto_rememberfor trait-enabled models- Cached read operations:
get,first,firstOrFail,find,findOrFail, and pagination methods - Nested eager-load aware cache keys
- Pagination-safe keys for
paginate(),simplePaginate(), andcursorPaginate() - Morph map versioning in cache keys
- Dependency index for precise invalidation on child updates
- Structured cache payloads for models and collections (safe for morph relations and eager loads)
- Cache tags when the selected Laravel cache driver supports them
- Model observer invalidation for saved, deleted, restored, and force deleted events
- Artisan commands for warming, inspecting, and flushing tracked entries
Installation
composer require ghostcompiler/laravel-model-caching
Publish the config:
php artisan vendor:publish --tag=model-cache-config
Redis is recommended for production because the dependency index can use native Redis sets.
Quick Start
Add the trait to models that should be cacheable:
use GhostCompiler\LaravelModelCaching\Concerns\HasModelCaching;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use HasModelCaching;
}
The trait alone does not cache queries. Either call remember() on the chain or enable auto_remember in config.
Use remember() on normal Eloquent queries:
$users = User::with('posts.comments')
->where('active', true)
->remember(60)
->get();
$tenant = User::whereHas('brand.domains', fn ($q) => $q->where('domain', $host))
->with('brand')
->remember(300)
->firstOrFail();
Enable automatic caching for all read queries on trait-enabled models:
// config/model-cache.php
'auto_remember' => true,
Then User::where('active', true)->first() is cached with default_ttl. Use dontCache() on chains that must always hit the database.
Use the convenience APIs:
$user = User::findCached(1);
$user->loadCached('posts.comments');
Disable caching on a query chain:
$users = User::remember(600)
->dontCache()
->where('active', true)
->get();
Cache Key Safety
Each key includes:
- Model class, table, and connection
- SQL and bindings
- Selected columns
- Limit, offset, groups, and orders
- Eager loads, including nested relation names
- Closure constraint source fingerprints when available
- Pagination page, page name, cursor, and per-page values
- Morph map and configured morph map version
- Optional auth and tenant context
These queries generate different keys:
User::with('posts')->remember(60)->get();
User::with('posts.comments')->remember(60)->get();
User::with(['posts', 'roles'])->remember(60)->get();
Dependency Invalidation
On a cache miss, the package stores the query result and walks the returned models plus loaded relations. It records only model primary keys in a dependency index:
model-cache:dependency:user:1 -> [cache-key-a]
model-cache:dependency:post:5 -> [cache-key-a, cache-key-b]
model-cache:dependency:comment:9 -> [cache-key-b]
When Post #5 is saved, deleted, or force deleted, only keys listed under model-cache:dependency:post:5 are forgotten. No full application cache flush is required.
When a new Post is created (or soft-deleted row is restored), every cached query rooted on that model class is forgotten too — for example paginated user lists — so new rows can appear on the next request without waiting for TTL expiry:
model-cache:dependency:user:{class-hash}:_class -> [page-1-key, page-2-key, ...]
Updates to an existing row still invalidate only that row's dependencies plus any list page that included that instance.
Cache tags are used as an extra layer when supported. Per-entry tag metadata is stored separately so entries can include result-specific tags such as post:5 while still being readable later from a deterministic query key.
Morph Relations
Polymorphic relations are supported through the actual related models returned by Eloquent and through morph context in the cache key. If you change Laravel's morph map aliases, bump:
'morph_map_version' => 2,
This prevents old entries from colliding with new alias meanings.
Pagination
Pagination methods are cached at the top-level builder operation:
$users = User::with('posts')->remember(300)->paginate(10);
The key includes the page name, current page, per-page value, and cursor state where applicable.
Configuration
return [
'enabled' => true,
'default_ttl' => 3600,
'auto_remember' => false,
'store' => null,
'prefix' => 'model-cache',
'cache_tags' => true,
'dependency_ttl' => 604800,
'use_redis_sets' => true,
'auto_observe_models' => true,
'include_auth_id' => false,
'context_callbacks' => [],
'morph_map_version' => 1,
'debug' => false,
];
For multi-tenant apps, add a context callback:
'context_callbacks' => [
'tenant' => fn () => tenant('id'),
],
Commands
Warm a query:
php artisan model-cache:warm "App\Models\User" --with=posts --with=posts.comments --ttl=600 --limit=1000
Inspect keys depending on a model instance:
php artisan model-cache:inspect "App\Models\Post" 5
Flush keys depending on a model instance:
php artisan model-cache:flush "App\Models\Post" 5
Flush every key known to the package dependency index:
php artisan model-cache:flush --known
Production Notes
- Treat
auto_remembercarefully: incidental reads on cacheable models are cached until TTL or model invalidation. UsedontCache()in admin or debug paths. - Prefer Redis for high traffic APIs.
- Keep cacheable queries deterministic.
- Include tenant/auth context when query results differ by user or tenant.
- Use eager loading intentionally. The package tracks loaded relations, not unloaded graph possibilities.
- Bump
morph_map_versionafter morph map changes. - Use
rememberForever()only for data that is always invalidated by model events.
Testing
composer install
composer test
Development And Build Environment
This package was developed using ServBay as the local development environment.
Development Tool Used
- Local development tool:
ServBay - Website: www.servbay.com
ServBay your development friend
Testing And Build Machine
- Tested on:
Mac M4 - Built on:
Mac M4
License
MIT