Files

129 lines
3.4 KiB
PHP

<?php
namespace App\Services\Cache;
use Closure;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use stdClass;
class CacheService implements CacheServiceInterface
{
private const TTL_SECONDS = [
self::TTL_SHORT => 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];
}
}