123 lines
3.6 KiB
PHP
123 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Core;
|
|
|
|
use PDO;
|
|
use PDOException;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* SQLite-basierte Datenbank für Test-/Entwicklungsumgebungen.
|
|
* Ersetzt die MySQL-Database-Klasse und emuliert MySQL-spezifische Features.
|
|
*/
|
|
final class DatabaseSqlite implements DatabaseInterface
|
|
{
|
|
private ?PDO $pdo = null;
|
|
private readonly string $dbPath;
|
|
|
|
public function __construct(private readonly array $config = [])
|
|
{
|
|
$this->dbPath = $this->config['path']
|
|
?? dirname(__DIR__, 2) . '/storage/database.sqlite';
|
|
}
|
|
|
|
public function pdo(): PDO
|
|
{
|
|
if ($this->pdo instanceof PDO) {
|
|
return $this->pdo;
|
|
}
|
|
|
|
$dbDir = dirname($this->dbPath);
|
|
|
|
if (!is_dir($dbDir) && !mkdir($dbDir, 0775, true) && !is_dir($dbDir)) {
|
|
throw new RuntimeException('Das Datenbank-Verzeichnis konnte nicht erstellt werden: ' . $dbDir);
|
|
}
|
|
|
|
try {
|
|
$this->pdo = new PDO(
|
|
'sqlite:' . $this->dbPath,
|
|
null,
|
|
null,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]
|
|
);
|
|
|
|
$this->pdo->exec('PRAGMA journal_mode=WAL');
|
|
$this->pdo->exec('PRAGMA foreign_keys=ON');
|
|
$this->pdo->exec('PRAGMA busy_timeout=5000');
|
|
} catch (PDOException $exception) {
|
|
throw new PDOException(
|
|
'SQLite connection failed: ' . $exception->getMessage(),
|
|
(int) $exception->getCode(),
|
|
$exception
|
|
);
|
|
}
|
|
|
|
return $this->pdo;
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->dbPath !== '';
|
|
}
|
|
|
|
/**
|
|
* Führt schema.sqlite.sql aus, um die Datenbank zu initialisieren.
|
|
*/
|
|
public function initializeSchema(): void
|
|
{
|
|
$schemaFile = dirname(__DIR__, 2) . '/database/schema.sqlite.sql';
|
|
|
|
if (!is_file($schemaFile)) {
|
|
throw new RuntimeException('SQLite-Schema nicht gefunden: ' . $schemaFile);
|
|
}
|
|
|
|
$schema = file_get_contents($schemaFile);
|
|
|
|
if (!is_string($schema) || $schema === '') {
|
|
throw new RuntimeException('SQLite-Schema konnte nicht geladen werden.');
|
|
}
|
|
|
|
$this->pdo()->exec($schema);
|
|
}
|
|
|
|
/**
|
|
* SQLite-kompatibler UPSERT: INSERT OR REPLACE + SELECT als Alternative zu ON DUPLICATE KEY.
|
|
*/
|
|
public function upsertRateLimit(string $actionKey, string $bucketKey, string $now): void
|
|
{
|
|
$pdo = $this->pdo();
|
|
|
|
$existing = $pdo->prepare(
|
|
'SELECT id FROM rate_limits WHERE action_key = :action_key AND bucket_key = :bucket_key LIMIT 1'
|
|
);
|
|
$existing->execute([
|
|
'action_key' => $actionKey,
|
|
'bucket_key' => $bucketKey,
|
|
]);
|
|
|
|
if (!$existing->fetchColumn()) {
|
|
$insert = $pdo->prepare(
|
|
'INSERT INTO rate_limits (action_key, bucket_key, attempts, window_start, last_attempt_at, created_at)
|
|
VALUES (:action_key, :bucket_key, 0, :window_start, :last_attempt_at, :created_at)'
|
|
);
|
|
$insert->execute([
|
|
'action_key' => $actionKey,
|
|
'bucket_key' => $bucketKey,
|
|
'window_start' => $now,
|
|
'last_attempt_at' => $now,
|
|
'created_at' => $now,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function getDbPath(): string
|
|
{
|
|
return $this->dbPath;
|
|
}
|
|
} |