175 lines
5.1 KiB
PHP
175 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database\Middleware;
|
|
|
|
use App\Framework\Database\Cache\CacheStrategy;
|
|
use App\Framework\Database\Cache\QueryCacheKey;
|
|
use App\Framework\Database\Exception\DatabaseException;
|
|
|
|
final readonly class CacheMiddleware implements QueryMiddleware
|
|
{
|
|
public function __construct(
|
|
private CacheStrategy $cacheStrategy,
|
|
private int $defaultTtlSeconds = 300, // 5 Minuten
|
|
private bool $enabled = true,
|
|
private array $cacheableOperations = ['query', 'queryOne', 'queryColumn', 'queryScalar']
|
|
) {}
|
|
|
|
public function process(QueryContext $context, callable $next): mixed
|
|
{
|
|
if (!$this->enabled || !$this->isCacheable($context)) {
|
|
return $next($context);
|
|
}
|
|
|
|
$cacheKey = $this->generateCacheKey($context);
|
|
|
|
// Versuche aus Cache zu lesen
|
|
$cachedResult = $this->cacheStrategy->get($cacheKey);
|
|
if ($cachedResult !== null) {
|
|
// Cache Hit - speichere Statistik
|
|
$context = $context->withMetadata('cache_hit', true);
|
|
$context = $context->withMetadata('cache_key', $cacheKey->toString());
|
|
|
|
return $this->deserializeResult($cachedResult, $context->operation);
|
|
}
|
|
|
|
// Cache Miss - führe Query aus
|
|
$result = $next($context);
|
|
|
|
// Speichere Ergebnis im Cache
|
|
$ttl = $this->determineTtl($context);
|
|
$serializedResult = $this->serializeResult($result, $context->operation);
|
|
|
|
$this->cacheStrategy->set($cacheKey, $serializedResult, $ttl);
|
|
|
|
// Speichere Cache-Metadaten
|
|
$context = $context->withMetadata('cache_hit', false);
|
|
$context = $context->withMetadata('cache_key', $cacheKey->toString());
|
|
$context = $context->withMetadata('cache_ttl', $ttl);
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 30; // Niedrige Priorität - nach HealthCheck und Retry
|
|
}
|
|
|
|
private function isCacheable(QueryContext $context): bool
|
|
{
|
|
// Nur bestimmte Operationen sind cacheable
|
|
if (!in_array($context->operation, $this->cacheableOperations, true)) {
|
|
return false;
|
|
}
|
|
|
|
// Transaktions-Queries nicht cachen
|
|
if ($context->connection->inTransaction()) {
|
|
return false;
|
|
}
|
|
|
|
// Prüfe auf non-cacheable SQL-Patterns
|
|
$sql = strtoupper(trim($context->sql));
|
|
|
|
// SELECT-Statements sind normalerweise cacheable
|
|
if (!str_starts_with($sql, 'SELECT')) {
|
|
return false;
|
|
}
|
|
|
|
// Bestimmte SELECT-Patterns nicht cachen
|
|
$nonCacheablePatterns = [
|
|
'NOW()',
|
|
'CURRENT_TIMESTAMP',
|
|
'RAND()',
|
|
'RANDOM()',
|
|
'UUID()',
|
|
'CURRENT_USER',
|
|
'CONNECTION_ID()',
|
|
];
|
|
|
|
foreach ($nonCacheablePatterns as $pattern) {
|
|
if (str_contains($sql, $pattern)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function generateCacheKey(QueryContext $context): QueryCacheKey
|
|
{
|
|
return new QueryCacheKey(
|
|
$context->sql,
|
|
$context->parameters,
|
|
$context->connection
|
|
);
|
|
}
|
|
|
|
private function determineTtl(QueryContext $context): int
|
|
{
|
|
// Prüfe auf custom TTL in Metadaten
|
|
if ($context->hasMetadata('cache_ttl')) {
|
|
return (int) $context->getMetadata('cache_ttl');
|
|
}
|
|
|
|
// Intelligente TTL basierend auf Query-Pattern
|
|
$sql = strtoupper(trim($context->sql));
|
|
|
|
// Lange TTL für statische/referenz Daten
|
|
if (str_contains($sql, 'INFORMATION_SCHEMA') ||
|
|
str_contains($sql, 'SHOW TABLES') ||
|
|
str_contains($sql, 'DESCRIBE ')) {
|
|
return 3600; // 1 Stunde
|
|
}
|
|
|
|
// Mittlere TTL für Aggregationen
|
|
if (str_contains($sql, 'COUNT(') ||
|
|
str_contains($sql, 'SUM(') ||
|
|
str_contains($sql, 'AVG(') ||
|
|
str_contains($sql, 'GROUP BY')) {
|
|
return 900; // 15 Minuten
|
|
}
|
|
|
|
// Standard TTL
|
|
return $this->defaultTtlSeconds;
|
|
}
|
|
|
|
private function serializeResult(mixed $result, string $operation): array
|
|
{
|
|
return [
|
|
'operation' => $operation,
|
|
'data' => $result,
|
|
'timestamp' => time(),
|
|
'serialized_at' => microtime(true),
|
|
];
|
|
}
|
|
|
|
private function deserializeResult(array $cachedData, string $operation): mixed
|
|
{
|
|
// Validiere dass Operation übereinstimmt
|
|
if ($cachedData['operation'] !== $operation) {
|
|
throw new DatabaseException(
|
|
"Cache operation mismatch: expected {$operation}, got {$cachedData['operation']}"
|
|
);
|
|
}
|
|
|
|
return $cachedData['data'];
|
|
}
|
|
|
|
public function invalidatePattern(string $pattern): int
|
|
{
|
|
return $this->cacheStrategy->invalidatePattern($pattern);
|
|
}
|
|
|
|
public function invalidateAll(): void
|
|
{
|
|
$this->cacheStrategy->clear();
|
|
}
|
|
|
|
public function getCacheStats(): array
|
|
{
|
|
return $this->cacheStrategy->getStats();
|
|
}
|
|
}
|