Auto-Invalidation
Overview
Automatically invalidate caches when related models are modified, ensuring data consistency without manual cache management.
How It Works
The auto-invalidation system observes Eloquent model events and automatically flushes associated cache tags when models are created, updated, deleted, restored, or force deleted.
graph LR
A[Model Event] --> B[Observer]
B --> C{Config Mapping}
C --> D[Get Tags]
D --> E[Flush Caches]
Configuration
Enable Auto-Invalidation
// config/filterable.php
'cache' => [
'auto_invalidate' => [
'enabled' => true,
'models' => [
App\Models\Post::class => ['posts', 'content'],
App\Models\User::class => ['users'],
App\Models\Category::class => ['categories', 'posts'],
App\Models\Comment::class => ['comments', 'posts'],
],
],
],
Environment Variable
FILTERABLE_AUTO_INVALIDATE=true
Model-to-Tags Mapping
Define which cache tags should be flushed when each model changes:
'models' => [
// When Post model changes, flush 'posts' and 'content' tags
App\Models\Post::class => ['posts', 'content'],
// When User model changes, flush 'users' tag
App\Models\User::class => ['users'],
// When Category changes, flush both 'categories' and 'posts'
// (because posts might be filtered by category)
App\Models\Category::class => ['categories', 'posts'],
],
Usage Example
Step 1: Configure Tags
// config/filterable.php
'auto_invalidate' => [
'enabled' => true,
'models' => [
App\Models\Post::class => ['posts', 'blog'],
],
],
Step 2: Use Tags in Filters
// In your filter
$posts = Post::filter()
->cache(3600)
->cacheTags(['posts', 'blog']) // Match config tags
->get();
Step 3: Automatic Invalidation
// When a post is created, updated, or deleted...
$post = Post::create([...]);
// ✅ All caches with 'posts' and 'blog' tags are automatically flushed!
$post->update([...]);
// ✅ Caches flushed again!
$post->delete();
// ✅ Caches flushed!
Observed Events
The system automatically handles these Eloquent events:
| Event | Description |
|---|---|
created | Model is created |
updated | Model is updated |
deleted | Model is soft or hard deleted |
restored | Model is restored from soft delete |
forceDeleted | Model is permanently deleted |
Advanced Patterns
Cascade Invalidation
When one model affects multiple cache types:
'models' => [
// Post changes affect multiple cache groups
App\Models\Post::class => [
'posts',
'blog',
'content',
'homepage',
'feeds',
],
// Category changes affect posts and navigation
App\Models\Category::class => [
'categories',
'posts', // Posts filtered by category
'navigation', // Site navigation with category counts
],
],
Relationship-Based Invalidation
Flush caches when related models change:
'models' => [
// Comment changes should invalidate post caches
App\Models\Comment::class => [
'comments',
'posts', // Because post.comments_count might change
],
// Tag changes affect posts
App\Models\Tag::class => [
'tags',
'posts', // Posts filtered by tags
],
],
Multi-Tenant Invalidation
Handle tenant-specific cache invalidation:
// Instead of global tags, use tenant-scoped caching
$posts = Post::filter()
->cache(3600)
->scopeByTenant($tenantId)
->cacheTags(['posts', "tenant:{$tenantId}"])
->get();
// Config maps to tenant-aware tags
'models' => [
App\Models\Post::class => ['posts'], // Global invalidation
// Tenant-specific handled by scoping
],
Performance Considerations
Tag Granularity
Balance between granular control and performance:
// ❌ Too granular (many small tag flushes)
'models' => [
App\Models\Post::class => [
'posts',
'posts:published',
'posts:draft',
'posts:archived',
'posts:category:1',
'posts:category:2',
// ...
],
],
// ✅ Good balance (reasonable tag count)
'models' => [
App\Models\Post::class => ['posts', 'content'],
App\Models\Category::class => ['categories', 'posts'],
],
Conditional Invalidation
Only invalidate when specific fields change:
// In your model
class Post extends Model
{
protected static function booted()
{
// Only invalidate cache if published status changed
static::updated(function ($post) {
if ($post->isDirty('status')) {
\Kettasoft\Filterable\Caching\FilterableCacheManager::getInstance()
->flushByTags(['posts:published']);
}
});
}
}
Cache Tracking
Monitor auto-invalidation events:
// config/filterable.php
'cache' => [
'tracking' => [
'enabled' => true,
'log_channel' => 'daily',
],
],
Log entries will show:
[2024-01-15 10:30:45] daily.INFO: Filterable cache invalidated
{
"model": "App\\Models\\Post",
"tags": ["posts", "content"],
"timestamp": "2024-01-15T10:30:45+00:00"
}
Manual Invalidation
Override automatic behavior when needed:
// Flush specific tags manually
\Kettasoft\Filterable\Caching\FilterableCacheManager::getInstance()
->flushByTags(['posts']);
// Or use static method
Post::flushCacheByTagsStatic(['posts', 'content']);
// Flush all caches for a specific filter
$filter = new PostFilter();
$filter->flushCache(); // Uses auto-generated class tag
Testing Auto-Invalidation
Test that caches are properly invalidated:
use Tests\TestCase;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
class CacheInvalidationTest extends TestCase
{
/** @test */
public function it_invalidates_cache_when_post_is_updated()
{
$post = Post::factory()->create();
// Cache some data
$cached = Post::filter()
->cache(3600)
->cacheTags(['posts'])
->get();
$this->assertCount(1, $cached);
// Update the post (should trigger invalidation)
$post->update(['title' => 'Updated']);
// Cache should be cleared, fresh data retrieved
$fresh = Post::filter()
->cache(3600)
->cacheTags(['posts'])
->get();
$this->assertEquals('Updated', $fresh->first()->title);
}
}
Common Patterns
Pattern 1: Blog System
'auto_invalidate' => [
'enabled' => true,
'models' => [
// Posts
App\Models\Post::class => ['posts', 'blog', 'feed'],
// Comments invalidate posts too
App\Models\Comment::class => ['comments', 'posts'],
// Categories affect post listings
App\Models\Category::class => ['categories', 'posts'],
// Tags affect post filtering
App\Models\Tag::class => ['tags', 'posts'],
// Authors affect post author pages
App\Models\User::class => ['users', 'authors'],
],
],
Pattern 2: E-commerce
'auto_invalidate' => [
'enabled' => true,
'models' => [
// Products
App\Models\Product::class => ['products', 'catalog'],
// Categories affect product listings
App\Models\Category::class => ['categories', 'products'],
// Inventory changes affect availability
App\Models\Inventory::class => ['inventory', 'products'],
// Prices affect product displays
App\Models\Price::class => ['prices', 'products'],
// Reviews affect product ratings
App\Models\Review::class => ['reviews', 'products'],
],
],
Pattern 3: Multi-Tenant SaaS
'auto_invalidate' => [
'enabled' => true,
'models' => [
// Tenant-level resources
App\Models\Tenant::class => ['tenants'],
// User changes within tenant
App\Models\User::class => ['users'],
// Tenant-specific data
App\Models\Project::class => ['projects'],
App\Models\Task::class => ['tasks', 'projects'],
],
],
// Use scoped caching per tenant
$projects = Project::filter()
->cache(3600)
->scopeByTenant(tenant()->id)
->cacheTags(['projects'])
->get();
Troubleshooting
Cache Not Invalidating
Check if auto-invalidation is enabled:
config('filterable.cache.auto_invalidate.enabled')Verify model is in config:
// Make sure model class is correctly namespaced App\Models\Post::class => ['posts']Check cache driver supports tags:
# Tags require Redis or Memcached php artisan config:show cache.defaultEnable tracking to see logs:
'tracking' => ['enabled' => true]
Observer Not Firing
Clear config cache:
php artisan config:clearVerify observer is registered:
php artisan route:list | grep CacheInvalidationObserverCheck model events are firing:
Post::observe(new class { public function updated($post) { logger('Post updated: ' . $post->id); } });
Best Practices
✅ Use hierarchical tags:
['posts', 'posts:published', 'content']✅ Keep model mappings simple:
- 2-4 tags per model is ideal
- Avoid over-granular tags
✅ Test invalidation:
- Write tests for critical cache flows
- Monitor logs in production
✅ Document tag usage:
- Maintain a tag registry
- Document which features use which tags
❌ Avoid:
- Too many tags (performance overhead)
- Missing required tags (stale caches)
- Circular dependencies (redundant flushes)
