ebects / laravel-cache-group
Group-based cache invalidation for Laravel โ organize cache by groups, invalidate by scope (user/role/custom), with automatic Redis Cluster compatibility.
v1.0.7
2026-02-22 12:25 UTC
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/cache: ^10.0|^11.0|^12.0
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/redis: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
- predis/predis: ^2.0
README
Group-based cache invalidation for Laravel โ organize cache by groups, invalidate by scope (user/role/custom), with automatic Redis Cluster compatibility.
Features
- ๐ฆ Cache Groups โ organize cache per feature/module with a dedicated class
- ๐ฏ Scoped Invalidation โ per user, role, tenant, or any custom scope
- ๐ Cascading Dependencies โ invalidate related groups automatically via
also_invalidate - ๐ก๏ธ Redis Cluster Ready โ auto-detect single/cluster mode, zero config
- โก Throttle/Debounce โ protect against invalidation storms
- ๐ Queue Safe โ explicit scope resolution for jobs without auth context
- ๐ Pluggable Auth โ works with Sanctum, JWT, Passport, or any custom auth
- ๐ Artisan Commands โ inspect, validate, and monitor cache groups
- ๐งช Testing Utilities โ FakeCacheManager with assertion helpers
- ๐ CacheGroupStore โ scope-aware
remember()andrememberResource()helpers
Installation
composer require ebects/laravel-cache-group
Publish the config (optional):
php artisan vendor:publish --tag=cache-group-config
Quick Start
1. Create a Cache Group
<?php namespace App\Cache\Groups; use App\Actions\Posts\CreatePost; use App\Actions\Posts\UpdatePost; use App\Actions\Posts\FetchPosts; use Ebects\LaravelCacheGroup\Contracts\CacheGroupInterface; class PostsCacheGroup implements CacheGroupInterface { public static function getPrefix(): string { return 'posts.list'; } public static function getConfig(): array { return [ 'scope' => 'user', // 'global', 'user', 'role', or any custom scope 'ttl' => 3600, // 1 hour 'also_invalidate' => [ // cascade invalidation 'dashboard.stats', ], ]; } public static function getClasses(): array { return [FetchPosts::class]; // classes that READ this cache } public static function getRemoveClasses(): array { return [ CreatePost::class, // simple: invalidates this group UpdatePost::class => ['posts.detail'], // also invalidates posts.detail ]; } }
2. Register Cache Groups
In a service provider (e.g. AppServiceProvider):
use Ebects\LaravelCacheGroup\CacheRegistry; use App\Cache\Groups\PostsCacheGroup; use App\Cache\Groups\DashboardCacheGroup; public function boot(): void { CacheRegistry::registerMany([ PostsCacheGroup::class, DashboardCacheGroup::class, ]); }
3. Store Cache with CacheGroupStore (READ side)
use Ebects\LaravelCacheGroup\CacheGroupStore; // In controller โ auto-resolves user/role from auth $posts = CacheGroupStore::remember( 'posts.list', // prefix 'list', // variant fn() => Post::paginate(20), // callback extraParams: request()->all() // unique per filter/page ); // Resource/detail cache โ scope-aware! $post = CacheGroupStore::rememberResource( 'posts.list', // prefix 'detail', // variable name $postId, // resource ID fn() => Post::findOrFail($postId) // callback );
4. Invalidate Cache (WRITE side)
use Ebects\LaravelCacheGroup\Traits\InvalidatesCache; class CreatePost { use InvalidatesCache; public function handle(array $data): Post { $post = Post::create($data); // Auto-detect which caches to invalidate from class mapping $this->invalidateCache(); return $post; } }
5. Implement ScopeResolver
<?php namespace App\Cache; use Ebects\LaravelCacheGroup\Contracts\ScopeResolver; class AppScopeResolver implements ScopeResolver { public function resolve(string $scope): ?string { $user = auth()->user(); if (!$user) return null; return match($scope) { 'user' => (string) $user->id, 'role' => $user->role->slug, 'tenant' => (string) $user->tenant_id, default => null, }; } public function resolveFor(string $scope, mixed $target): ?string { if (is_string($target)) return $target; return match($scope) { 'user' => (string) $target->id, 'role' => is_string($target) ? $target : $target->role->slug, 'tenant' => (string) $target->tenant_id, default => null, }; } public function hasActiveSession(): bool { return auth()->check(); } }
Register in config:
// config/cache-group.php 'scope_resolver' => App\Cache\AppScopeResolver::class,
CacheGroupStore API
The READ side โ storing and retrieving cached data with automatic scope handling.
remember() โ HTTP context (auto-resolve scope)
// Simple $data = CacheGroupStore::remember('dashboard.stats', 'summary', fn() => expensive()); // With request params for unique key per page/filter $data = CacheGroupStore::remember('posts.list', 'list', fn() => Post::paginate(), extraParams: request()->all()); // Custom TTL $data = CacheGroupStore::remember('reports', 'monthly', fn() => generateReport(), ttl: 7200);
rememberFor() โ Queue/CLI context (explicit scope)
// In queue job โ no auth session available $data = CacheGroupStore::rememberFor( 'dashboard.stats', 'summary', 'user', $userId, // explicit scope + target fn() => expensiveCalculation() );
rememberResource() โ Resource/detail cache (scope-aware)
// Auto-resolve scope from auth $detail = CacheGroupStore::rememberResource( 'surat_masuk.main', // prefix 'detail', // variable name $suratId, // resource ID fn() => Surat::findOrFail($suratId) ); // Explicit scope for queue $detail = CacheGroupStore::rememberResourceFor( 'surat_masuk.main', 'detail', $suratId, 'user', $userId, fn() => Surat::findOrFail($suratId) );
Utility methods
// Check if cache exists CacheGroupStore::has('posts.list', 'list'); // Forget specific entry CacheGroupStore::forget('posts.list', 'list');
Invalidation API
The WRITE side โ clearing cache when data changes.
Using Trait (recommended)
use Ebects\LaravelCacheGroup\Traits\InvalidatesCache; class UpdatePost { use InvalidatesCache; public function handle(Post $post, array $data): Post { $post->update($data); // Auto from class mapping (HTTP context) $this->invalidateCache(); // Explicit scope + target (queue safe) $this->invalidateCacheFor('user', $userId); // Specific prefix $this->invalidateCacheByPrefix('posts.list'); // Nuclear โ all users $this->invalidateAllCache('posts.list'); // Multiple targets $this->invalidateCacheForMany('user', [$userId1, $userId2]); return $post; } }
Using Facade
use Ebects\LaravelCacheGroup\Facades\CacheGroup; CacheGroup::invalidate('posts.list'); CacheGroup::invalidateFor('posts.list', 'user', $userId); CacheGroup::invalidateAll('master.data'); CacheGroup::forceInvalidate('dashboard.stats');
Force Invalidate with Throttle
$result = $this->forceInvalidateCache('dashboard.stats'); if ($result['throttled']) { return response()->json([ 'message' => "Please wait {$result['retry_after']} seconds", ], 429); }
Artisan Commands
# List all registered cache groups php artisan cache:groups php artisan cache:groups --scope=user # Inspect Redis keys for a cache group (read-only) php artisan cache:inspect posts.list php artisan cache:inspect --summary # Validate configuration & health check php artisan cache:validate
Redis Cluster Support
The library auto-detects your Redis setup:
| Redis Setup | Strategy | How It Works |
|---|---|---|
| Single Redis | Cache::tags()->flush() |
Fast, precise |
| Redis Cluster | SCAN + DEL per node |
Compatible, safe |
No config needed. To force a mode:
// config/cache-group.php 'redis_cluster_mode' => true, // force cluster 'redis_cluster_mode' => false, // force tags 'redis_cluster_mode' => null, // auto-detect (default)
Testing
use Ebects\LaravelCacheGroup\CacheManager; use Ebects\LaravelCacheGroup\Testing\FakeCacheManager; public function test_creating_post_invalidates_cache() { $fake = new FakeCacheManager(); $this->app->instance(CacheManager::class, $fake); $action = new CreatePost(); $action->handle(['title' => 'Test']); $fake->assertInvalidated('posts.list'); $fake->assertNotInvalidated('unrelated.prefix'); $fake->assertInvalidationCount(2); }
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
- Redis (single or cluster)
Support
If this package helps you, consider buying me a coffee โ
License
MIT License. See LICENSE for details.