300, // 5 minutes self::TTL_MEDIUM => 3600, // 1 hour self::TTL_LONG => 86400, // 24 hours ]; private const TAGGABLE_DRIVERS = ['redis', 'memcached', 'dynamodb']; // ------------------------------------------------------------------------- // Interface implementation // ------------------------------------------------------------------------- public function remember( string $key, Closure $callback, string $ttl = self::TTL_MEDIUM, array $tags = [], ): mixed { $store = $this->resolveStore($tags); $sentinel = new stdClass(); $cached = $store->get($key, $sentinel); if ($cached !== $sentinel) { Log::debug("CACHE HIT: {$key}"); return $cached; } Log::debug("CACHE MISS: {$key}"); $value = $callback(); $store->put($key, $value, $this->resolveTtl($ttl)); return $value; } public function put( string $key, mixed $value, string $ttl = self::TTL_MEDIUM, array $tags = [], ): void { $this->resolveStore($tags)->put($key, $value, $this->resolveTtl($ttl)); } public function get(string $key, mixed $default = null): mixed { return Cache::get($key, $default); } public function forget(string $key): void { Cache::forget($key); } public function clearByModule(string $module): void { if ($this->supportsTagging()) { Cache::tags([$module])->flush(); return; } $this->scanAndDelete($module); } // ------------------------------------------------------------------------- // Internals // ------------------------------------------------------------------------- private function resolveStore(array $tags): CacheRepository { if (!empty($tags) && $this->supportsTagging()) { return Cache::tags($tags); } return Cache::store(); } /** * Delete all keys for a module using SCAN (safe for production). * * KEYS is O(N) and blocks Redis; SCAN iterates in batches without blocking. * The pattern matches: {cache_prefix}:{module}:* */ private function scanAndDelete(string $module): void { $redis = Redis::connection('cache'); $prefix = config('cache.prefix'); $pattern = "{$prefix}:{$module}:*"; $cursor = '0'; do { $result = $redis->scan($cursor, 'MATCH', $pattern, 'COUNT', 100); if ($result === false) { break; } [$cursor, $keys] = $result; foreach ((array) $keys as $key) { $redis->del($key); } } while ((string) $cursor !== '0'); } private function supportsTagging(): bool { return in_array(config('cache.default'), self::TAGGABLE_DRIVERS, strict: true); } private function resolveTtl(string $ttl): int { return self::TTL_SECONDS[$ttl] ?? self::TTL_SECONDS[self::TTL_MEDIUM]; } }