Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
{
Expand All @@ -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
)));
}
}
```
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -224,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

Expand All @@ -237,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

Expand Down
22 changes: 21 additions & 1 deletion src/CacheUiLaravel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(mb_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
Expand Down
5 changes: 5 additions & 0 deletions src/Commands/CacheUiLaravelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
95 changes: 95 additions & 0 deletions tests/Unit/CacheUiLaravelFileDriverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

use Abr4xas\CacheUiLaravel\CacheUiLaravel;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;

describe('CacheUiLaravel File Driver Deletion', function (): void {
beforeEach(function (): void {
$this->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();
});
});