TmpSqliteCache.php
PHP
Path: src/Cache/TmpSqliteCache.php
<?php
namespace mini\Cache;
use Psr\SimpleCache\CacheInterface;
/**
* SQLite-backed PSR-16 SimpleCache implementation for /tmp
*
* Stores cache data in SQLite database in temporary directory.
* Lightweight alternative to DatabaseCache that doesn't require DatabaseInterface.
*/
class TmpSqliteCache implements CacheInterface
{
private \PDO $pdo;
private string $tableName = 'cache';
public function __construct(?string $dbPath = null)
{
$dbPath = $dbPath ?? sys_get_temp_dir() . '/mini-cache.sqlite3';
$this->pdo = new \PDO('sqlite:' . $dbPath);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->ensureTableExists();
}
/**
* Ensure the cache table exists
*/
private function ensureTableExists(): void
{
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS {$this->tableName} (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
");
$this->pdo->exec("
CREATE INDEX IF NOT EXISTS idx_cache_expires_at
ON {$this->tableName} (expires_at)
");
}
/**
* Validate cache key
*/
private function validateKey(string $key): void
{
if (empty($key)) {
throw new \InvalidArgumentException('Cache key cannot be empty');
}
// PSR-16 specifies these characters are not allowed: {}()/\@
if (preg_match('/[{}()\/@\\\]/', $key)) {
throw new \InvalidArgumentException('Cache key contains invalid characters: ' . $key);
}
}
/**
* Calculate expiration timestamp from TTL
*/
private function calculateExpiration(null|int|\DateInterval $ttl): ?int
{
if ($ttl === null) {
return null; // No expiration
}
if ($ttl instanceof \DateInterval) {
$now = new \DateTime();
$expires = $now->add($ttl);
return $expires->getTimestamp();
}
if ($ttl <= 0) {
return time() - 1; // Already expired
}
return time() + $ttl;
}
public function get(string $key, mixed $default = null): mixed
{
$this->validateKey($key);
$stmt = $this->pdo->prepare("SELECT value, expires_at FROM {$this->tableName} WHERE key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return $default;
}
// Check if expired
if ($row['expires_at'] !== null && $row['expires_at'] < time()) {
$this->delete($key);
return $default;
}
return unserialize($row['value']);
}
public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
{
$this->validateKey($key);
$serializedValue = serialize($value);
$expiresAt = $this->calculateExpiration($ttl);
try {
$stmt = $this->pdo->prepare(
"INSERT OR REPLACE INTO {$this->tableName} (key, value, expires_at, created_at)
VALUES (?, ?, ?, ?)"
);
$stmt->execute([$key, $serializedValue, $expiresAt, time()]);
return true;
} catch (\Exception $e) {
return false;
}
}
public function delete(string $key): bool
{
$this->validateKey($key);
try {
$stmt = $this->pdo->prepare("DELETE FROM {$this->tableName} WHERE key = ?");
$stmt->execute([$key]);
return true;
} catch (\Exception $e) {
return false;
}
}
public function clear(): bool
{
try {
$this->pdo->exec("DELETE FROM {$this->tableName}");
return true;
} catch (\Exception $e) {
return false;
}
}
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->get($key, $default);
}
return $result;
}
public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool
{
$success = true;
foreach ($values as $key => $value) {
if (!$this->set($key, $value, $ttl)) {
$success = false;
}
}
return $success;
}
public function deleteMultiple(iterable $keys): bool
{
$success = true;
foreach ($keys as $key) {
if (!$this->delete($key)) {
$success = false;
}
}
return $success;
}
public function has(string $key): bool
{
$this->validateKey($key);
$stmt = $this->pdo->prepare("SELECT expires_at FROM {$this->tableName} WHERE key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return false;
}
// Check if expired
if ($row['expires_at'] !== null && $row['expires_at'] < time()) {
$this->delete($key);
return false;
}
return true;
}
/**
* Manually trigger garbage collection (remove expired entries)
*/
public function cleanup(): int
{
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM {$this->tableName}");
$stmt->execute();
$before = $stmt->fetchColumn();
$this->pdo->prepare(
"DELETE FROM {$this->tableName} WHERE expires_at IS NOT NULL AND expires_at < ?"
)->execute([time()]);
$stmt->execute();
$after = $stmt->fetchColumn();
return $before - $after;
}
}