diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8c85ed9..02ca3b7 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -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.* diff --git a/README.md b/README.md index d400f17..5f58cfd 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/composer.json b/composer.json index 30ec666..8eec58e 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/Commands/CacheUiLaravelCommand.php b/src/Commands/CacheUiLaravelCommand.php index 8c1f047..ef32fd0 100644 --- a/src/Commands/CacheUiLaravelCommand.php +++ b/src/Commands/CacheUiLaravelCommand.php @@ -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("📝 Key: {$selectedKey}"); - $this->line("💾 Value: {$valuePreview}"); $this->newLine(); $confirmed = confirm( @@ -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; @@ -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 []; } @@ -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 []; } @@ -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 []; } @@ -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 '(null)'; - } + if (! File::exists($filePath)) { + return null; + } - if (is_bool($value)) { - return $value ? 'true' : '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).'...'; + // 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).'...'; + $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; + } } } diff --git a/tests/Unit/CacheUiLaravelCommandSimpleTest.php b/tests/Unit/CacheUiLaravelCommandSimpleTest.php index d287cbe..51ddd3f 100644 --- a/tests/Unit/CacheUiLaravelCommandSimpleTest.php +++ b/tests/Unit/CacheUiLaravelCommandSimpleTest.php @@ -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 { @@ -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('(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('true'); - expect($falseResult)->toBe('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 ...'); - }); - - 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('...'); - }); -}); - describe('getCacheKeys method', function (): void { it('returns array for any driver type', function (): void { $command = new CacheUiLaravelCommand(); diff --git a/tests/Unit/CacheUiLaravelCommandTest.php b/tests/Unit/CacheUiLaravelCommandTest.php new file mode 100644 index 0000000..8155478 --- /dev/null +++ b/tests/Unit/CacheUiLaravelCommandTest.php @@ -0,0 +1,78 @@ +getMethod('getArrayKeys'); + + $result = $method->invoke($command); + expect($result)->toBeArray(); + expect($result)->toBeEmpty(); + }); + }); + + describe('handleUnsupportedDriver method', function (): void { + it('exists and is private', function (): void { + $command = new CacheUiLaravelCommand(); + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('handleUnsupportedDriver'); + + expect($method->isPrivate())->toBeTrue(); + expect($method->getReturnType()->getName())->toBe('array'); + }); + }); + + describe('getFileKeyValue method', function (): void { + it('returns null when file does not exist', function (): void { + $command = new CacheUiLaravelCommand(); + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('getFileKeyValue'); + + $result = $method->invoke($command, 'nonexistent_file'); + expect($result)->toBeNull(); + }); + }); + + describe('deleteFileKey method', function (): void { + it('returns false when file does not exist', function (): void { + $command = new CacheUiLaravelCommand(); + $reflection = new ReflectionClass($command); + $method = $reflection->getMethod('deleteFileKey'); + + $result = $method->invoke($command, 'nonexistent_file'); + expect($result)->toBeFalse(); + }); + }); + + describe('method existence', function (): void { + it('has all required private methods', function (): void { + $command = new CacheUiLaravelCommand(); + $reflection = new ReflectionClass($command); + + $expectedMethods = [ + 'getCacheKeys', + 'getRedisKeys', + 'getFileKeys', + 'getDatabaseKeys', + 'getArrayKeys', + 'handleUnsupportedDriver', + 'getFileKeyValue', + 'deleteFileKeyByKey', + 'deleteFileKey', + ]; + + foreach ($expectedMethods as $methodName) { + expect($reflection->hasMethod($methodName))->toBeTrue(); + + $method = $reflection->getMethod($methodName); + expect($method->isPrivate())->toBeTrue(); + } + }); + }); +}); diff --git a/tests/Unit/CacheUiLaravelFacadeTest.php b/tests/Unit/CacheUiLaravelFacadeTest.php new file mode 100644 index 0000000..345257b --- /dev/null +++ b/tests/Unit/CacheUiLaravelFacadeTest.php @@ -0,0 +1,48 @@ +getParentClass()->getName())->toBe(Facade::class); + }); + + it('is final class', function (): void { + $reflection = new ReflectionClass(CacheUiLaravel::class); + expect($reflection->isFinal())->toBeTrue(); + }); + + it('has correct facade accessor', function (): void { + $reflection = new ReflectionClass(CacheUiLaravel::class); + $method = $reflection->getMethod('getFacadeAccessor'); + + $accessor = $method->invoke(null); + expect($accessor)->toBe(Abr4xas\CacheUiLaravel\CacheUiLaravel::class); + }); + }); + + describe('Facade functionality', function (): void { + it('has correct facade accessor method', function (): void { + $reflection = new ReflectionClass(CacheUiLaravel::class); + $method = $reflection->getMethod('getFacadeAccessor'); + + expect($method->isProtected())->toBeTrue(); + expect($method->isStatic())->toBeTrue(); + }); + }); + + describe('Documentation', function (): void { + it('has proper docblock', function (): void { + $reflection = new ReflectionClass(CacheUiLaravel::class); + $docComment = $reflection->getDocComment(); + + expect($docComment)->toContain('@see'); + expect($docComment)->toContain('CacheUiLaravel'); + }); + }); +}); diff --git a/tests/Unit/CacheUiLaravelServiceProviderTest.php b/tests/Unit/CacheUiLaravelServiceProviderTest.php new file mode 100644 index 0000000..acbe555 --- /dev/null +++ b/tests/Unit/CacheUiLaravelServiceProviderTest.php @@ -0,0 +1,66 @@ +toBeInstanceOf(CacheUiLaravelServiceProvider::class); + }); + + it('extends Laravel ServiceProvider', function (): void { + $app = Mockery::mock(Illuminate\Contracts\Foundation\Application::class); + $serviceProvider = new CacheUiLaravelServiceProvider($app); + $reflection = new ReflectionClass($serviceProvider); + + expect($reflection->getParentClass()->getName())->toBe(Illuminate\Support\ServiceProvider::class); + }); + + it('is final class', function (): void { + $reflection = new ReflectionClass(CacheUiLaravelServiceProvider::class); + expect($reflection->isFinal())->toBeTrue(); + }); + + it('has required methods', function (): void { + $reflection = new ReflectionClass(CacheUiLaravelServiceProvider::class); + + expect($reflection->hasMethod('boot'))->toBeTrue(); + expect($reflection->hasMethod('register'))->toBeTrue(); + + $bootMethod = $reflection->getMethod('boot'); + $registerMethod = $reflection->getMethod('register'); + + expect($bootMethod->isPublic())->toBeTrue(); + expect($registerMethod->isPublic())->toBeTrue(); + }); + }); + + describe('register method', function (): void { + it('registers CacheUiLaravel binding', function (): void { + $app = Mockery::mock(Illuminate\Contracts\Foundation\Application::class); + $app->shouldReceive('singleton')->once()->with( + CacheUiLaravel::class, + Mockery::type('Closure') + ); + + $serviceProvider = new CacheUiLaravelServiceProvider($app); + $serviceProvider->register(); + }); + }); + + describe('boot method', function (): void { + it('has correct method signature', function (): void { + $reflection = new ReflectionClass(CacheUiLaravelServiceProvider::class); + $method = $reflection->getMethod('boot'); + + expect($method->isPublic())->toBeTrue(); + expect($method->getReturnType()->getName())->toBe('void'); + }); + }); +});