bunkuris / has-cache
HasCache trait for Laravel models with cache key managers
Installs: 21
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/bunkuris/has-cache
Requires
- php: ^8.2
- ext-json: *
- chillerlan/php-qrcode: ^5.0
- illuminate/auth: ^10.0|^11.0|^12.0
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/events: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/pagination: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- illuminate/validation: ^10.0|^11.0|^12.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- larastan/larastan: ^2.4|^3.0
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.0|^10.0|^11.5
This package is auto-updated.
Last update: 2025-10-06 20:53:22 UTC
README
HasCache is a package that provides automatic cache invalidation for Laravel Eloquent models. It monitors model lifecycle events (create, update, delete) and automatically clears associated cache keys, preventing stale data without manual cache management. The package includes Redis-optimized async cache deletion, working hours-aware TTL management, and a type-safe cache key manager system.
Features
- Automatic Cache Invalidation: Clears cache automatically when models are saved, updated, or deleted
- Transaction-Aware: Respects database transactions and clears cache after commit
- Async Cache Deletion: Non-blocking cache clearing using Redis UNLINK command
- Working Hours TTL: Different cache TTLs for business hours vs off-hours
- Type-Safe Cache Keys: PHPStan-friendly cache key managers with IDE autocomplete
- Testing Helpers: Built-in testing utilities to assert cache operations
- Temporary Disable: Can temporarily disable cache purging when needed
- Chunk Processing: Efficiently handles large numbers of cache keys
Requirements
- PHP 8.2+
- Laravel 10.0+
Installation
You can install this package as a typical composer package.
composer require bunkuris/has-cache
Publish the configuration file (optional):
php artisan vendor:publish --tag=has-cache-config
Basic Usage
Step 1: Add the HasCache Trait to Your Model
<?php namespace App\Models; use Bunkuris\Traits\HasCache; use Illuminate\Database\Eloquent\Model; class User extends Model { /** @use HasCache<User> */ use HasCache; public function getCacheKeys(): array { return [ "user:{$this->id}:profile" => true, "user:{$this->id}:posts" => true, "users:list" => true, ]; } }
Now whenever a User model is saved, updated, or deleted, the specified cache keys will be automatically cleared.
Step 2: Use Cache Keys in Your Application
// The cache will be automatically cleared when the user is updated $profile = Cache::remember("user:{$userId}:profile", 3600, function () use ($userId) { return User::find($userId)->getProfileData(); });
Advanced Usage
Using Cache Key Managers
Cache Key Managers provide a type-safe, organized way to manage cache keys with IDE autocomplete support.
Create a Cache Key Manager
php artisan cache:make:manager User
This creates a new manager at app/Support/Cache/UserCacheKeyManager.php
:
<?php namespace App\Support\Cache; use Bunkuris\Support\AbstractCacheKeyManager; use Bunkuris\Support\CacheKey; class UserCacheKeyManager extends AbstractCacheKeyManager { /** * Get cache key for user profile data * * @return CacheKey */ public static function getProfileCacheKey(int $id): CacheKey { return static::buildCacheKey('user_profile', [ 'id' => $id, ]); } /** * Get cache key for user posts * * @return CacheKey */ public static function getPostsCacheKey(int $id): CacheKey { return static::buildCacheKey('user_posts', [ 'id' => $id, ]); } /** * Get cache key for all users list * * @return CacheKey */ public static function getUsersListCacheKey(): CacheKey { return static::buildCacheKey('user_list'); } /** * Returns the available templates for this cache key manager. * * @return TemplateArray */ public static function getTemplates(): array { return [ 'user_profile' => [ 'pattern' => 'users:{id}:profile', 'in_working_hours_ttl' => Carbon::now()->addHour(), 'after_working_hours_ttl' => Carbon::now()->addHours(2), ], 'user_posts' => [ 'pattern' => 'users:{id}:posts', 'in_working_hours_ttl' => Carbon::now()->addMinutes(30), ], 'user_list' => [ 'pattern' => 'users:list', 'in_working_hours_ttl' => Carbon::now()->addMinutes(10), 'after_working_hours_ttl' => Carbon::now()->addMinutes(20), ], ]; } }
Use the Cache Key Manager in Your Model
<?php namespace App\Models; use App\Support\Cache\UserCacheKeyManager; use Bunkuris\Traits\HasCache; use Illuminate\Database\Eloquent\Model; class User extends Model { use HasCache; public function getCacheKeys(): array { return [ (string) UserCacheKeyManager::getProfileCacheKey($this->id) => true, (string) UserCacheKeyManager::getPostsCacheKey($this->id) => true, (string) UserCacheKeyManager::getUsersListCacheKey() => true, ]; } }
Use Cache Keys Throughout Your Application
use App\Support\Cache\UserCacheKeyManager; // Remember cached data with type safety $profile = UserCacheKeyManager::getProfileCacheKey($userId)->remember(fn() => [ 'name' => $user->name, 'email' => $user->email, 'avatar' => $user->avatar_url, ]); // Check if cached if (UserCacheKeyManager::getProfileCacheKey($userId)->cached()) { // Cache exists } // Manually forget cache UserCacheKeyManager::getProfileCacheKey($userId)->forget(); // Put data in cache UserCacheKeyManager::getProfileCacheKey($userId)->put($profileData); // Get from cache (without default) $data = UserCacheKeyManager::getProfileCacheKey($userId)->get();
Working Hours TTL
By default, cache keys use different TTLs based on working hours (configurable in config/has-cache.php
):
return [ 'active_hour' => [ 'start' => 8, // 8 AM 'end' => 20, // 8 PM ], ];
During working hours (8 AM - 8 PM), the default TTL is 30 minutes. Outside working hours, it's 12 hours. You can customize this per cache key:
public static function getProfileCacheKey(int $userId): CacheKey { return new CacheKey( key: "user:{$userId}:profile", in_active_hours_ttl: Carbon::now()->addHour(), // 1 hour during working hours after_active_hours_ttl: Carbon::now()->addHours(24), // 24 hours outside working hours ); }
Or use the same TTL regardless of time:
public static function getProfileCacheKey(int $userId): CacheKey { return new CacheKey( key: "user:{$userId}:profile", in_active_hours_ttl: Carbon::now()->addHour(), // 1 hour ttl no matter the current time of day. after_active_hours_ttl: null, ); }
Temporarily Disable Cache Purging
Sometimes you need to update models without clearing cache:
use App\Models\User; User::withoutCachePurge(function () { // These updates won't clear cache User::where('status', 'inactive')->update(['last_checked' => now()]); }); // Or for a single operation $user->withoutCachePurge(function ($user) { $user->increment('login_count'); });
Transaction-Aware Cache Clearing
Cache is automatically cleared after database transactions commit:
$cacheKey = UserCacheKeyManager::getProfileCacheKey($userId); DB::transaction(function () use ($cacheKey) { $user = User::find(1); $user->name = 'New Name'; $user->save(); User::clearCacheAfterCommit([$cacheKey]); }); // Cache is cleared here, after transaction commits
If the transaction rolls back, cache won't be cleared.
Manual Async Cache Deletion
You can manually delete multiple cache keys asynchronously:
use Bunkuris\Facades\AsyncCache; AsyncCache::deleteMultipleAsync([ 'user:1:profile', 'user:1:posts', 'user:2:profile', 'user:2:posts', ], chunkSize: 1000);
This uses Redis UNLINK for non-blocking deletion and processes keys in chunks for efficiency.
Testing
The package provides testing helpers to assert cache operations in your tests.
Setup Test Case
<?php namespace Tests; use Bunkuris\Testing\InteractsWithAsyncCache; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication; use InteractsWithAsyncCache; }
The InteractsWithAsyncCache
trait automatically resets the cache state before each test, so you don't need to manually call resetAsyncCache()
between tests.
Assert Cache Operations
use App\Models\User; use App\Support\Cache\UserCacheKeyManager; public function test_user_update_clears_cache(): void { $user = User::factory()->create(); // Update the user $user->update(['name' => 'New Name']); // Assert cache keys were deleted $this->assertCacheKeyDeleted( (string) UserCacheKeyManager::getProfileCacheKey($user->id) ); // Assert multiple keys $this->assertCacheKeysDeleted([ (string) UserCacheKeyManager::getProfileCacheKey($user->id), (string) UserCacheKeyManager::getPostsCacheKey($user->id), ]); // Assert exact count $this->assertCacheKeyCount(2); }
You can still manually reset the cache state mid-test if needed:
public function test_multiple_operations(): void { $user = User::factory()->create(); $this->assertCacheKeyCount(2); // Reset cache tracking $this->resetAsyncCache(); $user->update(['name' => 'Updated']); $this->assertCacheKeyCount(2); }
Configuration
The configuration file config/has-cache.php
allows you to customize working hours:
<?php return [ /* |-------------------------------------------------------------------------- | Active Hours Configuration |-------------------------------------------------------------------------- | | Define the working/active hours for your application. | Cache TTLs can be different during and outside these hours. | */ 'active_hour' => [ 'start' => env('CACHE_ACTIVE_HOUR_START', 8), // 8 AM 'end' => env('CACHE_ACTIVE_HOUR_END', 20), // 8 PM ], ];
How It Works
- Model Lifecycle Hooks: The
HasCache
trait registers listeners forupdating
,saved
, anddeleted
events - Cache Key Collection: Before updates, it stores the original cache keys from
getCacheKeys()
- Transaction Detection: It detects if the operation is within a database transaction
- Async Deletion: After save/delete (or after transaction commit), it triggers async cache deletion
- Redis Optimization: If using Redis, it uses the UNLINK command for non-blocking deletion
- Chunk Processing: Large key sets are processed in chunks (default: 1000) for efficiency
Performance Considerations
- Redis UNLINK: Non-blocking deletion doesn't impact application response time
- Chunked Processing: Large cache key sets are processed in batches
- Transaction-Aware: Cache clearing waits for transaction commit, preventing unnecessary work on rollbacks
- Working Hours TTL: Longer cache during off-hours reduces database load
API Reference
HasCache Trait
// Get cache keys for this model instance public function getCacheKeys(): array // Temporarily disable cache purging public static function withoutCachePurge(callable $callback): mixed // Manually clear cache after transaction commit public static function clearCacheAfterCommit(array $cacheKeys): void
CacheKey Class
// Create a new cache key new CacheKey( string $key, Carbon|int $in_active_hours_ttl, Carbon|int|null $after_active_hours_ttl = null ) // Cache operations public function remember(Closure $callback): mixed public function forget(): bool public function put(mixed $value): void public function get(mixed $default = null): mixed public function cached(): bool // Get the key as string public function __toString(): string
AsyncCache Facade
// Delete multiple cache keys asynchronously AsyncCache::deleteMultipleAsync(array $keys, int $chunkSize = 1000): bool
Testing Assertions
// Assert a cache key was deleted $this->assertCacheKeyDeleted(string $key, string $message = ''): void // Assert multiple cache keys were deleted $this->assertCacheKeysDeleted(array $keys, string $message = ''): void // Assert exact number of cache keys deleted $this->assertCacheKeyCount(int $expectedCount, string $message = ''): void // Manually reset cache tracking mid-test $this->resetAsyncCache(): void
Contributing
We'll appreciate your collaboration to this package.
When making pull requests, make sure:
- All tests are passing:
composer test
- Test coverage is maintained at 100%:
composer test-coverage
- There are no PHPStan errors:
composer phpstan
- Coding standard is followed:
composer lint
orcomposer fix-style
to automatically fix it
Start the development environment:
cd docker
docker-compose up -d
Run tests inside the container:
docker exec -it has-cache-php composer test
Run tests with coverage:
docker exec -it has-cache-php composer test-coverage
Run tests with html coverage:
docker exec -it has-cache-php composer test-coverage
Run tests with HTML coverage:
docker exec -it has-cache-php composer test-coverage-html
License
This package is open-sourced software licensed under the MIT license.