From df3b2c4271ec9c8d1a9847fc332ea2e01066a111 Mon Sep 17 00:00:00 2001 From: Angel Date: Fri, 21 Nov 2025 14:17:03 -0300 Subject: [PATCH 1/3] fix: file driver deletion bug and docs update --- README.md | 35 +++++++++++++++----------- src/Commands/CacheUiLaravelCommand.php | 5 ++++ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 068c5aa..da4d0d3 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,13 @@ CACHE_UI_PREVIEW_LIMIT=150 CACHE_UI_SEARCH_SCROLL=20 ``` -### Custom File Cache Driver (Recommended) +### Custom File Cache Driver (Only for File Store) -For the best experience with file cache, you can use our custom `key-aware-file` driver that allows Cache UI to display real keys instead of file hashes. +If you are using the `file` cache driver (default in Laravel), you should use our custom `key-aware-file` driver. + +**Why?** The standard Laravel `file` driver stores keys as hashes, making them unreadable. This custom driver wraps the value to store the real key, allowing you to see and search for them. + +> **Important**: This is **NOT** needed for Redis or Database drivers, as they support listing keys natively. #### Driver Configuration @@ -84,6 +88,7 @@ namespace App\Providers; use Abr4xas\CacheUiLaravel\KeyAwareFileStore; use Illuminate\Support\Facades\Cache; use Illuminate\Support\ServiceProvider; +use Illuminate\Foundation\Application; class AppServiceProvider extends ServiceProvider { @@ -101,13 +106,11 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { // Register the custom file cache driver - Cache::extend('key-aware-file', function ($app, $config) { - return Cache::repository(new KeyAwareFileStore( - $app['files'], - $config['path'], - $config['file_permission'] ?? null - )); - }); + Cache::extend('key-aware-file', fn (Application $app, array $config) => Cache::repository(new KeyAwareFileStore( + $app['files'], + $config['path'], + $config['file_permission'] ?? null + ))); } } ``` @@ -156,11 +159,15 @@ php artisan cache:list --store=redis ### Supported Drivers -- ✅ **Redis**: Lists all keys using Redis KEYS command -- ✅ **File**: Reads cache files from the filesystem -- ✅ **Database**: Queries the cache table in the database -- ⚠️ **Array**: Not supported (array driver doesn't persist between requests) -- ⚠️ **Memcached**: Not currently supported +| Driver | Support | Configuration Required | +|--------|---------|------------------------| +| **Redis** | ✅ Native | None (Works out of the box) | +| **Database** | ✅ Native | None (Works out of the box) | +| **File** | ✅ Enhanced | **Requires `key-aware-file` driver** | +| **Array** | ⚠️ No | Not supported (doesn't persist) | +| **Memcached** | ⚠️ No | Not currently supported | + +> **Note**: The `key-aware-file` driver is **only** needed if you use the `file` cache driver. If you use Redis or Database, you don't need to change your driver configuration. ### Usage Example diff --git a/src/Commands/CacheUiLaravelCommand.php b/src/Commands/CacheUiLaravelCommand.php index d44627e..e390eaf 100644 --- a/src/Commands/CacheUiLaravelCommand.php +++ b/src/Commands/CacheUiLaravelCommand.php @@ -90,6 +90,11 @@ public function handle(): int if (! $deleted && $this->driver === 'file') { // For file driver, try to delete using the actual key $deleted = $this->deleteFileKeyByKey($selectedKey); + + if (! $deleted) { + // If that fails, it might be a standard cache file where the key is the filename (hash) + $deleted = $this->deleteFileKey($selectedKey); + } } if ($deleted) { From a862a755ac2ec090577108aaabd7abad726a4075 Mon Sep 17 00:00:00 2001 From: Angel Date: Fri, 21 Nov 2025 15:56:11 -0300 Subject: [PATCH 2/3] verify file driver cache deletion and update README --- README.md | 4 +- src/CacheUiLaravel.php | 22 ++++- tests/Unit/CacheUiLaravelFileDriverTest.php | 95 +++++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/CacheUiLaravelFileDriverTest.php diff --git a/README.md b/README.md index da4d0d3..94edce9 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ The following tests need to be implemented to fully validate the new `KeyAwareFi - [ ] Test complete cache workflow (store → retrieve → delete) - [ ] Test multiple keys with different expiration times - [ ] Test cache key listing with `getAllKeys()` method -- [ ] Test cache key deletion with `forgetKey()` method +- [x] Test cache key deletion with `forgetKey()` method - [ ] Test mixed wrapped and legacy data scenarios - [ ] Test performance with large numbers of cache keys @@ -244,7 +244,7 @@ The following tests need to be implemented to fully validate the new `KeyAwareFi ### CacheUiLaravel Integration Tests - [ ] Test `getAllKeys()` method with `key-aware-file` driver -- [ ] Test `forgetKey()` method with `key-aware-file` driver +- [x] Test `forgetKey()` method with `key-aware-file` driver - [ ] Test mixed driver scenarios (Redis + File + Database) - [ ] Test error handling and graceful degradation diff --git a/src/CacheUiLaravel.php b/src/CacheUiLaravel.php index 0da7fc7..073c5e0 100644 --- a/src/CacheUiLaravel.php +++ b/src/CacheUiLaravel.php @@ -33,9 +33,29 @@ public function getAllKeys(?string $store = null): array */ public function forgetKey(string $key, ?string $store = null): bool { + $storeName = $store ?? config('cache.default'); $cacheStore = $store !== null && $store !== '' && $store !== '0' ? Cache::store($store) : Cache::store(); - return $cacheStore->forget($key); + if ($cacheStore->forget($key)) { + return true; + } + + // Handle file driver specific logic for hashed keys + $driver = config("cache.stores.{$storeName}.driver"); + + if (in_array($driver, ['file', 'key-aware-file']) && preg_match('/^[a-f0-9]{40}$/', $key)) { + // Use the path from the specific store configuration, fallback to default + $cachePath = config("cache.stores.{$storeName}.path", config('cache.stores.file.path', storage_path('framework/cache/data'))); + + $parts = array_slice(str_split($key, 2), 0, 2); + $path = $cachePath . '/' . implode('/', $parts) . '/' . $key; + + if (File::exists($path)) { + return File::delete($path); + } + } + + return false; } private function getRedisKeys(string $store): array diff --git a/tests/Unit/CacheUiLaravelFileDriverTest.php b/tests/Unit/CacheUiLaravelFileDriverTest.php new file mode 100644 index 0000000..2f51af0 --- /dev/null +++ b/tests/Unit/CacheUiLaravelFileDriverTest.php @@ -0,0 +1,95 @@ +cacheUiLaravel = new CacheUiLaravel(); + }); + + it('deletes hashed file when standard forget fails for file driver', function (): void { + // Mock Cache::forget to return false, simulating that it couldn't find the key by name + Cache::shouldReceive('store')->withNoArgs()->andReturnSelf(); + Cache::shouldReceive('forget')->with('008cb7ea48f292dd8b03d361a4c9f66085f77090')->andReturn(false); + + // Configure file driver + Config::set('cache.default', 'file'); + Config::set('cache.stores.file.driver', 'file'); + $cachePath = storage_path('framework/cache/data'); + Config::set('cache.stores.file.path', $cachePath); + + // The key is a SHA1 hash + $key = '008cb7ea48f292dd8b03d361a4c9f66085f77090'; + + // Expected file path reconstruction + // 00/8c/008cb7ea48f292dd8b03d361a4c9f66085f77090 + $expectedPath = $cachePath . '/00/8c/' . $key; + + // Mock File existence and deletion + File::shouldReceive('exists')->with($expectedPath)->andReturn(true); + File::shouldReceive('delete')->with($expectedPath)->andReturn(true); + + $result = $this->cacheUiLaravel->forgetKey($key); + + expect($result)->toBeTrue(); + }); + + it('deletes hashed file when standard forget fails for key-aware-file driver', function (): void { + // Mock Cache::forget to return false + Cache::shouldReceive('store')->withNoArgs()->andReturnSelf(); + Cache::shouldReceive('forget')->with('008cb7ea48f292dd8b03d361a4c9f66085f77090')->andReturn(false); + + // Configure key-aware-file driver + Config::set('cache.default', 'file'); + Config::set('cache.stores.file.driver', 'key-aware-file'); + $cachePath = storage_path('framework/cache/data'); + Config::set('cache.stores.file.path', $cachePath); + + $key = '008cb7ea48f292dd8b03d361a4c9f66085f77090'; + $expectedPath = $cachePath . '/00/8c/' . $key; + + File::shouldReceive('exists')->with($expectedPath)->andReturn(true); + File::shouldReceive('delete')->with($expectedPath)->andReturn(true); + + $result = $this->cacheUiLaravel->forgetKey($key); + + expect($result)->toBeTrue(); + }); + + it('does not attempt file deletion for non-hashed keys', function (): void { + Cache::shouldReceive('store')->withNoArgs()->andReturnSelf(); + Cache::shouldReceive('forget')->with('not-a-hash')->andReturn(false); + + Config::set('cache.default', 'file'); + Config::set('cache.stores.file.driver', 'file'); + + // File::exists/delete should NOT be called + File::shouldReceive('exists')->never(); + File::shouldReceive('delete')->never(); + + $result = $this->cacheUiLaravel->forgetKey('not-a-hash'); + + expect($result)->toBeFalse(); + }); + + it('does not attempt file deletion for non-file drivers', function (): void { + Cache::shouldReceive('store')->withNoArgs()->andReturnSelf(); + Cache::shouldReceive('forget')->with('008cb7ea48f292dd8b03d361a4c9f66085f77090')->andReturn(false); + + Config::set('cache.default', 'redis'); + Config::set('cache.stores.redis.driver', 'redis'); + + // File::exists/delete should NOT be called + File::shouldReceive('exists')->never(); + File::shouldReceive('delete')->never(); + + $result = $this->cacheUiLaravel->forgetKey('008cb7ea48f292dd8b03d361a4c9f66085f77090'); + + expect($result)->toBeFalse(); + }); +}); From d255f6dadceb7d93a685247e10fda4b1e9d8bec2 Mon Sep 17 00:00:00 2001 From: Angel Date: Fri, 21 Nov 2025 16:00:35 -0300 Subject: [PATCH 3/3] fix style --- src/CacheUiLaravel.php | 4 ++-- tests/Unit/CacheUiLaravelFileDriverTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CacheUiLaravel.php b/src/CacheUiLaravel.php index 073c5e0..c005000 100644 --- a/src/CacheUiLaravel.php +++ b/src/CacheUiLaravel.php @@ -47,8 +47,8 @@ public function forgetKey(string $key, ?string $store = null): bool // Use the path from the specific store configuration, fallback to default $cachePath = config("cache.stores.{$storeName}.path", config('cache.stores.file.path', storage_path('framework/cache/data'))); - $parts = array_slice(str_split($key, 2), 0, 2); - $path = $cachePath . '/' . implode('/', $parts) . '/' . $key; + $parts = array_slice(mb_str_split($key, 2), 0, 2); + $path = $cachePath.'/'.implode('/', $parts).'/'.$key; if (File::exists($path)) { return File::delete($path); diff --git a/tests/Unit/CacheUiLaravelFileDriverTest.php b/tests/Unit/CacheUiLaravelFileDriverTest.php index 2f51af0..9d5b7b8 100644 --- a/tests/Unit/CacheUiLaravelFileDriverTest.php +++ b/tests/Unit/CacheUiLaravelFileDriverTest.php @@ -28,7 +28,7 @@ // Expected file path reconstruction // 00/8c/008cb7ea48f292dd8b03d361a4c9f66085f77090 - $expectedPath = $cachePath . '/00/8c/' . $key; + $expectedPath = $cachePath.'/00/8c/'.$key; // Mock File existence and deletion File::shouldReceive('exists')->with($expectedPath)->andReturn(true); @@ -51,7 +51,7 @@ Config::set('cache.stores.file.path', $cachePath); $key = '008cb7ea48f292dd8b03d361a4c9f66085f77090'; - $expectedPath = $cachePath . '/00/8c/' . $key; + $expectedPath = $cachePath.'/00/8c/'.$key; File::shouldReceive('exists')->with($expectedPath)->andReturn(true); File::shouldReceive('delete')->with($expectedPath)->andReturn(true);