PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ] ); } return self::$instance; } public static function query(string $sql, array $params = []): \PDOStatement { $start = microtime(true); $stmt = self::get()->prepare($sql); $stmt->execute($params); DebugLog::sql($sql, $params, microtime(true) - $start); return $stmt; } public static function fetchAll(string $sql, array $params = []): array { return self::query($sql, $params)->fetchAll(); } public static function fetchOne(string $sql, array $params = []): array|false { return self::query($sql, $params)->fetch(); } /** Liefert einen Skalarwert der ersten Spalte, oder $default bei leerem Ergebnis. */ public static function fetchValue(string $sql, array $params = [], mixed $default = null): mixed { $wert = self::query($sql, $params)->fetchColumn(); return $wert === false ? $default : $wert; } public static function execute(string $sql, array $params = []): int { return self::query($sql, $params)->rowCount(); } public static function lastInsertId(): int { return (int)self::get()->lastInsertId(); } /** * Verschachtelbare Transaktionen: nur die äußerste öffnet und schließt * wirklich. Nötig, weil Tests bereits eine Transaktion halten, die * Anwendungslogik darin aber ihrerseits welche startet. */ public static function beginTransaction(): void { if (self::$txDepth === 0) { self::get()->beginTransaction(); } self::$txDepth++; } public static function commit(): void { if (self::$txDepth > 0) { self::$txDepth--; } if (self::$txDepth === 0 && self::get()->inTransaction()) { self::get()->commit(); } } public static function rollBack(): void { self::$txDepth = 0; if (self::get()->inTransaction()) { self::get()->rollBack(); } } /** Führt $fn in einer Transaktion aus und rollt bei jeder Ausnahme zurück. */ public static function transaktion(callable $fn): mixed { self::beginTransaction(); try { $ergebnis = $fn(); self::commit(); return $ergebnis; } catch (\Throwable $e) { self::rollBack(); throw $e; } } }