Finanzmanager/src/Database.php
fds2610-bot b1712bb7ea Phase 0: Projektgerüst mit nginx, php-fpm und MariaDB
Docker Compose mit drei Services (nginx auf Port 8000), Migrationsmechanik,
Update- und Backup-Skript. Anwendung: Router mit CSRF-Prüfung, Anmeldung und
Ersteinrichtung, zentraler Einstellungsdialog mit Reitern, ein- und
ausschaltbares Debug-Log mit Maskierung von Zugangsdaten.

Layout nach Vorbild des FinanzManagers: feste Navigation links,
Werkzeugleiste oben, Tab-Leiste; Farbschema hell/dunkel/System.

Tests: 51 PHP-Prüfungen und 7 Playwright-Tests, alle grün.
2026-08-09 20:21:42 +02:00

120 lines
3.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace Finanzmanager;
use Finanzmanager\Services\DebugLog;
use PDO;
/**
* Schmaler PDO-Wrapper. Sämtlicher SQL-Zugriff der Anwendung läuft über
* fetchAll/fetchOne/execute – ausschließlich mit Prepared Statements.
*/
class Database
{
private static ?PDO $instance = null;
private static int $txDepth = 0;
public static function get(): PDO
{
if (self::$instance === null) {
$host = Config::get('db_host');
$name = Config::get('db_name');
self::$instance = new PDO(
"mysql:host={$host};dbname={$name};charset=utf8mb4",
(string)Config::get('db_user'),
(string)Config::get('db_pass'),
[
PDO::ATTR_ERRMODE => 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;
}
}
}