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.

Maintainers

Package info

github.com/ebectsali/laravel-cache-group

pkg:composer/ebects/laravel-cache-group

Statistics

Installs: 35

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.7 2026-02-22 12:25 UTC

This package is auto-updated.

Last update: 2026-03-27 04:37:02 UTC


README

Latest Version on Packagist Total Downloads PHP Version License

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() and rememberResource() 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 โ˜•

Saweria

License

MIT License. See LICENSE for details.