chore: complete update
This commit is contained in:
174
src/Framework/Database/Middleware/CacheMiddleware.php
Normal file
174
src/Framework/Database/Middleware/CacheMiddleware.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
65
src/Framework/Database/Middleware/HealthCheckMiddleware.php
Normal file
65
src/Framework/Database/Middleware/HealthCheckMiddleware.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Middleware;
|
||||
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
use App\Framework\Database\HealthCheck\ConnectionHealthChecker;
|
||||
use App\Framework\Database\HealthCheck\HealthCheckResult;
|
||||
|
||||
final readonly class HealthCheckMiddleware implements QueryMiddleware
|
||||
{
|
||||
private ConnectionHealthChecker $healthChecker;
|
||||
|
||||
public function __construct(
|
||||
private int $healthCheckInterval = 30, // Sekunden
|
||||
private bool $enabled = true
|
||||
) {
|
||||
$this->healthChecker = new ConnectionHealthChecker();
|
||||
}
|
||||
|
||||
public function process(QueryContext $context, callable $next): mixed
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
// Prüfe ob Health Check notwendig ist
|
||||
$lastHealthCheck = $context->getMetadata('last_health_check', 0);
|
||||
$currentTime = time();
|
||||
|
||||
if (($currentTime - $lastHealthCheck) >= $this->healthCheckInterval) {
|
||||
$this->performHealthCheck($context);
|
||||
$context = $context->withMetadata('last_health_check', $currentTime);
|
||||
}
|
||||
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 80; // Hohe Priorität - vor Retry aber nach Lazy Loading
|
||||
}
|
||||
|
||||
private function performHealthCheck(QueryContext $context): void
|
||||
{
|
||||
$result = $this->healthChecker->checkHealth($context->connection);
|
||||
|
||||
if (!$result->isHealthy) {
|
||||
throw new DatabaseException(
|
||||
"Health check failed: {$result->message}",
|
||||
0,
|
||||
$result->exception
|
||||
);
|
||||
}
|
||||
|
||||
// Speichere Health Check Ergebnis im Kontext
|
||||
$context->metadata['health_check_result'] = $result;
|
||||
}
|
||||
|
||||
public function getLastHealthCheckResult(QueryContext $context): ?HealthCheckResult
|
||||
{
|
||||
return $context->getMetadata('health_check_result');
|
||||
}
|
||||
}
|
||||
67
src/Framework/Database/Middleware/MiddlewarePipeline.php
Normal file
67
src/Framework/Database/Middleware/MiddlewarePipeline.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Middleware;
|
||||
|
||||
final class MiddlewarePipeline
|
||||
{
|
||||
/** @var QueryMiddleware[] */
|
||||
private array $middleware = [];
|
||||
|
||||
public function add(QueryMiddleware $middleware): self
|
||||
{
|
||||
$this->middleware[] = $middleware;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addMultiple(array $middleware): self
|
||||
{
|
||||
foreach ($middleware as $m) {
|
||||
$this->add($m);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function process(QueryContext $context, callable $finalHandler): mixed
|
||||
{
|
||||
// Sortiere Middleware nach Priorität (höchste zuerst)
|
||||
$sortedMiddleware = $this->middleware;
|
||||
usort($sortedMiddleware, fn($a, $b) => $b->getPriority() <=> $a->getPriority());
|
||||
|
||||
// Baue die Pipeline von hinten nach vorne auf
|
||||
$pipeline = $finalHandler;
|
||||
|
||||
for ($i = count($sortedMiddleware) - 1; $i >= 0; $i--) {
|
||||
$middleware = $sortedMiddleware[$i];
|
||||
$pipeline = fn(QueryContext $ctx) => $middleware->process($ctx, $pipeline);
|
||||
}
|
||||
|
||||
return $pipeline($context);
|
||||
}
|
||||
|
||||
public function getMiddleware(): array
|
||||
{
|
||||
return $this->middleware;
|
||||
}
|
||||
|
||||
public function hasMiddleware(string $className): bool
|
||||
{
|
||||
foreach ($this->middleware as $middleware) {
|
||||
if ($middleware instanceof $className) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMiddlewareByType(string $className): ?QueryMiddleware
|
||||
{
|
||||
foreach ($this->middleware as $middleware) {
|
||||
if ($middleware instanceof $className) {
|
||||
return $middleware;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
35
src/Framework/Database/Middleware/QueryContext.php
Normal file
35
src/Framework/Database/Middleware/QueryContext.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Middleware;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
|
||||
final class QueryContext
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $operation,
|
||||
public readonly string $sql,
|
||||
public readonly array $parameters,
|
||||
public readonly ConnectionInterface $connection,
|
||||
public array $metadata = []
|
||||
) {}
|
||||
|
||||
public function withMetadata(string $key, mixed $value): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->metadata[$key] = $value;
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function getMetadata(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->metadata[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function hasMetadata(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->metadata);
|
||||
}
|
||||
}
|
||||
21
src/Framework/Database/Middleware/QueryMiddleware.php
Normal file
21
src/Framework/Database/Middleware/QueryMiddleware.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Middleware;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\ResultInterface;
|
||||
|
||||
interface QueryMiddleware
|
||||
{
|
||||
/**
|
||||
* Verarbeitet eine Query-Operation durch die Middleware
|
||||
*/
|
||||
public function process(QueryContext $context, callable $next): mixed;
|
||||
|
||||
/**
|
||||
* Priorität der Middleware (höhere Zahl = früher in der Pipeline)
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
}
|
||||
109
src/Framework/Database/Middleware/RetryMiddleware.php
Normal file
109
src/Framework/Database/Middleware/RetryMiddleware.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Middleware;
|
||||
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
use App\Framework\Database\HealthCheck\ConnectionHealthChecker;
|
||||
|
||||
final readonly class RetryMiddleware implements QueryMiddleware
|
||||
{
|
||||
private ConnectionHealthChecker $healthChecker;
|
||||
|
||||
public function __construct(
|
||||
private int $maxRetries = 3,
|
||||
private int $retryDelayMs = 100,
|
||||
private array $retryableExceptions = [
|
||||
\PDOException::class,
|
||||
DatabaseException::class,
|
||||
]
|
||||
) {
|
||||
$this->healthChecker = new ConnectionHealthChecker();
|
||||
}
|
||||
|
||||
public function process(QueryContext $context, callable $next): mixed
|
||||
{
|
||||
// Transaktions-Commits und Rollbacks nicht wiederholen
|
||||
if (in_array($context->operation, ['commit', 'rollback'], true)) {
|
||||
return $next($context);
|
||||
}
|
||||
|
||||
return $this->retryOperation($context, $next);
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 50; // Mittlere Priorität
|
||||
}
|
||||
|
||||
private function retryOperation(QueryContext $context, callable $next): mixed
|
||||
{
|
||||
$attempt = 0;
|
||||
$lastException = null;
|
||||
|
||||
while ($attempt <= $this->maxRetries) {
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (\Throwable $e) {
|
||||
$lastException = $e;
|
||||
|
||||
// Prüfe ob Exception retry-bar ist
|
||||
if (!$this->isRetryableException($e)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Prüfe ob wir noch Versuche haben
|
||||
if ($attempt >= $this->maxRetries) {
|
||||
break;
|
||||
}
|
||||
|
||||
$attempt++;
|
||||
|
||||
// Health Check vor Retry
|
||||
if (!$this->healthChecker->checkConnectionAlive($context->connection)) {
|
||||
// Für LazyGhost: Connection ist automatisch lazy und wird bei nächstem Zugriff neu initialisiert
|
||||
// Keine explizite Aktion nötig - LazyGhost handled das automatisch
|
||||
}
|
||||
|
||||
// Exponential Backoff
|
||||
$delay = $this->retryDelayMs * (2 ** ($attempt - 1));
|
||||
usleep($delay * 1000); // usleep benötigt Mikrosekunden
|
||||
}
|
||||
}
|
||||
|
||||
throw new DatabaseException(
|
||||
"Operation failed after {$this->maxRetries} retries. Last error: {$lastException->getMessage()}",
|
||||
0,
|
||||
$lastException
|
||||
);
|
||||
}
|
||||
|
||||
private function isRetryableException(\Throwable $exception): bool
|
||||
{
|
||||
foreach ($this->retryableExceptions as $retryableClass) {
|
||||
if ($exception instanceof $retryableClass) {
|
||||
// Prüfe spezifische Fehlercodes die nicht retry-bar sind
|
||||
if ($exception instanceof \PDOException) {
|
||||
return $this->isPdoExceptionRetryable($exception);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isPdoExceptionRetryable(\PDOException $exception): bool
|
||||
{
|
||||
// Bestimmte PDO-Fehler sind nicht retry-bar (z.B. Syntax-Fehler)
|
||||
$nonRetryableCodes = [
|
||||
'42000', // Syntax Error
|
||||
'42S02', // Table doesn't exist
|
||||
'42S22', // Column doesn't exist
|
||||
'23000', // Integrity constraint violation
|
||||
];
|
||||
|
||||
$sqlState = $exception->getCode();
|
||||
return !in_array($sqlState, $nonRetryableCodes, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user