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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
os: [ubuntu-latest]
php: [8.3]
laravel: [12.*]
stability: [prefer-lowest, prefer-stable]
stability: [prefer-stable]
include:
- laravel: 12.*
testbench: 10.*
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Are you sure you want to delete this cache key? › No / Yes
🗑️ The key 'user_1_profile' has been successfully deleted
```

### Programmatic Usage
### Programmatic Usage (optional)

You can also use the `CacheUiLaravel` class directly in your code:

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"minimum-stability": "stable",
"require": {
"php": "^8.3",
"illuminate/contracts": "^12.32.5",
"laravel/framework": "^12.32.5",
"laravel/prompts": "^0.3.7"
},
"require-dev": {
Expand Down
160 changes: 128 additions & 32 deletions src/Commands/CacheUiLaravelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,8 @@ public function handle(): int
return self::SUCCESS;
}

// Get the key value to show information
$value = $this->store->get($selectedKey);
$valuePreview = $this->getValuePreview($value);

$this->newLine();
$this->line("📝 <fg=cyan>Key:</> {$selectedKey}");
$this->line("💾 <fg=cyan>Value:</> {$valuePreview}");
$this->newLine();

$confirmed = confirm(
Expand All @@ -81,7 +76,23 @@ public function handle(): int
return self::SUCCESS;
}

if ($this->store->forget($selectedKey)) {
// Try to delete the key
// For Redis, we need to add the prefix back since we removed it when listing keys
if ($this->driver === 'redis') {
$prefix = config('database.redis.options.prefix', '');
$fullKey = $prefix ? $prefix.$selectedKey : $selectedKey;
$deleted = $this->store->forget($fullKey);
} else {
$deleted = $this->store->forget($selectedKey);
}

// If not deleted, try different approaches based on driver
if (! $deleted && $this->driver === 'file') {
// For file driver, try to delete using the actual key
$deleted = $this->deleteFileKeyByKey($selectedKey);
}

if ($deleted) {
info("🗑️ The key '{$selectedKey}' has been successfully deleted");

return self::SUCCESS;
Expand Down Expand Up @@ -119,7 +130,7 @@ private function getRedisKeys(): array
return $key;
}, $keys);
} catch (Exception $e) {
error('Error al obtener claves de Redis: '.$e->getMessage());
error('Error getting Redis keys: '.$e->getMessage());

return [];
}
Expand All @@ -138,17 +149,39 @@ private function getFileKeys(): array
$keys = [];

foreach ($files as $file) {
// El nombre del archivo en Laravel es un hash, pero podemos leer el contenido
$content = File::get($file->getPathname());

// Formato del archivo de caché de Laravel: expiration_time + serialized_value
// Intentar extraer el nombre de la clave del contenido serializado
$keys[] = $file->getFilename();
try {
$content = File::get($file->getPathname());

// Laravel file cache format: expiration_time + serialized_value
if (mb_strlen($content) < 10) {
continue;
}

$expiration = mb_substr($content, 0, 10);
$serialized = mb_substr($content, 10);

// Check if expired
if (time() > $expiration) {
continue;
}

// Try to unserialize to get the actual key
$data = unserialize($serialized);
if (is_array($data) && isset($data['key'])) {
$keys[] = $data['key'];
} else {
// Fallback to filename if we can't extract the key
$keys[] = $file->getFilename();
}
} catch (Exception) {
// If we can't read this file, skip it
continue;
}
}

return $keys;
} catch (Exception $e) {
error('Error al obtener claves del sistema de archivos: '.$e->getMessage());
error('Error getting file system keys: '.$e->getMessage());

return [];
}
Expand All @@ -161,7 +194,7 @@ private function getDatabaseKeys(): array

return DB::table($table)->pluck('key')->toArray();
} catch (Exception $e) {
error('Error al obtener claves de la base de datos: '.$e->getMessage());
error('Error getting database keys: '.$e->getMessage());

return [];
}
Expand All @@ -184,32 +217,95 @@ private function handleUnsupportedDriver(): array
return [];
}

private function getValuePreview(mixed $value): string
private function getFileKeyValue(string $filename): mixed
{
$previewLimit = config('cache-ui-laravel.preview_limit', 100);
try {
$cachePath = config('cache.stores.file.path', storage_path('framework/cache/data'));
$filePath = $cachePath.'/'.$filename;

if (is_null($value)) {
return '<fg=gray>(null)</>';
}
if (! File::exists($filePath)) {
return null;
}

if (is_bool($value)) {
return $value ? '<fg=green>true</>' : '<fg=red>false</>';
}
$content = File::get($filePath);

if (is_array($value) || is_object($value)) {
$json = json_encode($value, JSON_UNESCAPED_UNICODE);
if (mb_strlen($json) > $previewLimit) {
return mb_substr($json, 0, $previewLimit).'<fg=gray>...</>';
// Laravel file cache format: expiration_time + serialized_value
if (mb_strlen($content) < 10) {
return null;
}

return $json;
$expiration = mb_substr($content, 0, 10);
$serialized = mb_substr($content, 10);

// Check if expired
if (time() > $expiration) {
return null;
}

return unserialize($serialized);
} catch (Exception) {
return null;
}
}

private function deleteFileKeyByKey(string $key): bool
{
try {
$cachePath = config('cache.stores.file.path', storage_path('framework/cache/data'));

if (! File::exists($cachePath)) {
return false;
}

$stringValue = (string) $value;
if (mb_strlen($stringValue) > $previewLimit) {
return mb_substr($stringValue, 0, $previewLimit).'<fg=gray>...</>';
$files = File::allFiles($cachePath);

foreach ($files as $file) {
try {
$content = File::get($file->getPathname());

// Laravel file cache format: expiration_time + serialized_value
if (mb_strlen($content) < 10) {
continue;
}

$expiration = mb_substr($content, 0, 10);
$serialized = mb_substr($content, 10);

// Check if expired
if (time() > $expiration) {
continue;
}

// Try to unserialize to get the data
$data = unserialize($serialized);
if (is_array($data) && isset($data['key']) && $data['key'] === $key) {
return File::delete($file->getPathname());
}
} catch (Exception) {
// If we can't read this file, skip it
continue;
}
}

return false;
} catch (Exception) {
return false;
}
}

return $stringValue;
private function deleteFileKey(string $filename): bool
{
try {
$cachePath = config('cache.stores.file.path', storage_path('framework/cache/data'));
$filePath = $cachePath.'/'.$filename;

if (File::exists($filePath)) {
return File::delete($filePath);
}

return false;
} catch (Exception) {
return false;
}
}
}
83 changes: 0 additions & 83 deletions tests/Unit/CacheUiLaravelCommandSimpleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
declare(strict_types=1);

use Abr4xas\CacheUiLaravel\Commands\CacheUiLaravelCommand;
use Illuminate\Support\Facades\Config;

describe('CacheUiLaravelCommand Basic Tests', function (): void {
it('has correct signature and description', function (): void {
Expand All @@ -27,88 +26,6 @@
});
});

describe('getValuePreview method', function (): void {
it('handles null values', function (): void {
$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$result = $method->invoke($command, null);
expect($result)->toBe('<fg=gray>(null)</>');
});

it('handles boolean values', function (): void {
$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$trueResult = $method->invoke($command, true);
$falseResult = $method->invoke($command, false);

expect($trueResult)->toBe('<fg=green>true</>');
expect($falseResult)->toBe('<fg=red>false</>');
});

it('handles array values', function (): void {
$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$array = ['key' => 'value', 'number' => 123];
$result = $method->invoke($command, $array);

expect($result)->toBe('{"key":"value","number":123}');
});

it('handles object values', function (): void {
$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$object = (object) ['key' => 'value'];
$result = $method->invoke($command, $object);

expect($result)->toBe('{"key":"value"}');
});

it('handles string values within limit', function (): void {
$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$shortString = 'Hello World';
$result = $method->invoke($command, $shortString);

expect($result)->toBe('Hello World');
});

it('handles string values exceeding limit', function (): void {
Config::set('cache-ui-laravel.preview_limit', 10);

$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$longString = 'This is a very long string that exceeds the limit';
$result = $method->invoke($command, $longString);

expect($result)->toBe('This is a <fg=gray>...</>');
});

it('handles array values exceeding limit', function (): void {
Config::set('cache-ui-laravel.preview_limit', 20);

$command = new CacheUiLaravelCommand();
$reflection = new ReflectionClass($command);
$method = $reflection->getMethod('getValuePreview');

$largeArray = ['very' => 'long', 'array' => 'with', 'many' => 'keys', 'and' => 'values'];
$result = $method->invoke($command, $largeArray);

expect($result)->toContain('<fg=gray>...</>');
});
});

describe('getCacheKeys method', function (): void {
it('returns array for any driver type', function (): void {
$command = new CacheUiLaravelCommand();
Expand Down
Loading