mikebronner/laravel-model-caching
Speeds up Eloquent by automatically caching model queries and relationships, cutting repetitive database hits. Drop-in package with cache tagging support, configurable cache stores and TTLs, and easy invalidation on model updates—ideal for high-traffic Laravel apps.
Installation:
composer require genealabs/laravel-model-caching
(Auto-discovery handles the service provider.)
Apply the Trait:
Add Cachable to your base model or individual models:
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
class Post extends Model
{
use Cachable;
}
First Use Case: Query normally—caching and invalidation happen automatically:
$posts = Post::with('comments')->where('published', true)->paginate();
No manual cache keys or invalidation needed.
config/laravel-model-caching.php (publish with php artisan modelCache:publish --config).Post) to test caching behavior.Verify Caching:
Check if queries are hitting the cache by inspecting the cache store (e.g., Redis CLI: KEYS *).
Use dd() on query results to compare execution times.
Check Invalidation: Trigger a model update/delete and verify cache keys disappear:
php artisan modelCache:clear # Clears all cached queries
Logs:
Enable debug logging in config/laravel-model-caching.php:
'debug' => env('MODEL_CACHE_DEBUG', false),
Apply Trait:
Extend Cachable to models that need caching (e.g., Post, User).
Avoid applying to models with high write churn (e.g., Comment) unless using cool-down.
Query Patterns:
with() for relationships to cache them:
$posts = Post::with('comments.tags')->get();
select(): Custom column selections bypass caching.Post::disableCache()->where('active', false)->get();
Invalidation:
save(), delete(), or forceDelete() on cached models.ModelCache::invalidate() for custom logic:
use GeneaLabs\LaravelModelCaching\Facades\ModelCache;
ModelCache::invalidate(Post::class);
Multi-Tenancy:
Use $cachePrefix per model or globally in config:
class Post extends Model
{
use Cachable;
protected $cachePrefix = 'tenant-' . auth()->id();
}
High-Churn Models:
Implement cool-down for models like Comment:
class Comment extends Model
{
use Cachable;
protected $cacheCooldownSeconds = 300; // 5-minute cooldown
}
Activate in queries:
Comment::withCacheCooldownSeconds()->create([...]);
Transactions: Cache invalidation is deferred until the transaction commits. Manually flush if needed:
DB::transaction(function () {
Post::create([...]);
// Cache is not flushed yet
});
ModelCache::flush(); // Force flush
Testing:
Use disableCache() in tests to avoid stale data:
$this->withoutModelCaching(function () {
Post::factory()->create();
});
Cache Store Isolation:
Dedicate a Redis/Memcached instance for model caching by configuring a custom store in config/cache.php:
'model-cache' => [
'driver' => 'redis',
'connection' => 'model_cache',
],
Then set:
MODEL_CACHE_STORE=model-cache
Dynamic Cache Keys:
Override getCacheKey() in your model for custom key logic:
public function getCacheKey()
{
return 'custom:key:' . $this->id;
}
Conditional Caching: Disable caching for specific queries using a closure:
$posts = Post::when(fn () => !request()->wantsJson(), function ($query) {
return $query->cached();
})->get();
Cache Warmers: Pre-load cache during low-traffic periods (e.g., cron jobs):
Post::with('comments')->get(); // Warms cache
Event-Based Invalidation: Listen to model events for granular control:
Post::saved(function ($post) {
ModelCache::invalidate(Post::class);
});
Lazy-Loaded Relationships:
Only eager-loaded (with()) relationships are cached. Lazy loads bypass the cache:
$post = Post::find(1);
$post->comments; // Not cached!
Raw Queries:
Bypass cache if using DB::table() or query builder without Eloquent:
DB::table('posts')->get(); // Not cached!
Transactions:
Cache invalidation is deferred until the transaction commits. Use ModelCache::flush() manually if needed.
DynamoDB Quirks:
MODEL_CACHE_FALLBACK_TO_DB=true to avoid read failures during outages.Cool-Down Misuse:
Forgetting to call withCacheCooldownSeconds() means the cooldown is ignored:
// ❌ No cooldown activated
Comment::create([...]);
// ✅ Cooldown activated
Comment::withCacheCooldownSeconds()->create([...]);
Multi-Database Keying:
Disable use-database-keying if you share cache across databases manually:
'use-database-keying' => false,
Cache Key Inspection: Log cache keys to understand what’s being cached:
Post::addGlobalCacheListener(function ($key, $value) {
\Log::debug("Cached: {$key}");
});
Redis CLI: Inspect cached queries in Redis:
redis-cli keys "*"
redis-cli get "posts:active:page:1"
Disable Caching: Temporarily disable caching to isolate issues:
Post::disableCache()->get();
Or globally:
MODEL_CACHE_ENABLED=false
Check Invalidation: Verify invalidation works by:
php artisan modelCache:clear # Clear all
php artisan tinker
>>> \Cache::has('posts:active:page:1') # Should return false
Cool-Down Debugging: Check if cool-down is active:
$comment = Comment::withCacheCooldownSeconds()->create([...]);
\Log::debug($comment->freshTimestamp()); // Should show cooldown metadata
Cache Prefix:
config/laravel-model-caching.php:
'cache-prefix' => 'app_',
protected $cachePrefix = 'tenant_' . $this->tenantId;
Fallback Behavior:
fallback-to-database to avoid exceptions during cache outages:
MODEL_CACHE_FALLBACK_TO_DB=true
DynamoDB TTL:
expires_at attribute in DynamoDB.Debug Mode: Enable debug logging for troubleshooting:
'debug' => true,
Logs cache hits/misses and invalidation events.
Custom Cache Store:
Extend the package to support additional stores (e.g., ArangoDB) by implementing GeneaLabs\LaravelModelCaching\Contracts\CacheStore.
Event Listeners: Hook into cache events for custom logic:
ModelCache::listen('beforeInvalid
How can I help you explore Laravel packages today?