connect_error) { throw new Exception("Database connection failed: " . $conn->connect_error); } // Set charset to utf8mb4 for proper Unicode support $conn->set_charset('utf8mb4'); // Pin the MySQL session time zone to the app's configured zone so that // NOW()/CURRENT_TIMESTAMP and PHP agree on wall-clock time regardless of // the DB server's SYSTEM tz. Prefer the named zone (requires the // mysql.time_zone_* tables); if that isn't available, fall back to the // fixed numeric offset PHP computes for the same zone. Best-effort: a // failure here must never fatal the connection. $tz = $GLOBALS['config']['TIMEZONE'] ?? 'UTC'; try { $escaped = $conn->real_escape_string($tz); try { // mysqli throws (does not return false) on failure under the // default PHP 8.1+ report mode, so catch it rather than testing // the return value. $conn->query("SET time_zone = '{$escaped}'"); } catch (\Throwable $inner) { // Named zone unavailable (mysql.time_zone_* not populated) — fall // back to a fixed numeric offset so PHP and MySQL still agree on // wall-clock time regardless of the DB server's SYSTEM tz. $offset = (new DateTime('now', new DateTimeZone($tz)))->format('P'); $escapedOffset = $conn->real_escape_string($offset); $conn->query("SET time_zone = '{$escapedOffset}'"); } } catch (\Throwable $e) { error_log('Database: failed to set session time_zone: ' . $e->getMessage()); } return $conn; } /** * Close the database connection */ public static function close(): void { if (self::$connection !== null) { self::$connection->close(); self::$connection = null; } } /** * Begin a transaction * * @return bool Success */ public static function beginTransaction(): bool { return self::getConnection()->begin_transaction(); } /** * Commit a transaction * * @return bool Success */ public static function commit(): bool { return self::getConnection()->commit(); } /** * Rollback a transaction * * @return bool Success */ public static function rollback(): bool { return self::getConnection()->rollback(); } /** * Execute a query and return results * * @param string $sql SQL query with placeholders * @param string $types Parameter types (i=int, s=string, d=double, b=blob) * @param array $params Parameters to bind * @return mysqli_result|bool Query result */ public static function query(string $sql, string $types = '', array $params = []) { $conn = self::getConnection(); if (empty($types) || empty($params)) { return $conn->query($sql); } $stmt = $conn->prepare($sql); if (!$stmt) { throw new Exception("Query preparation failed: " . $conn->error); } $stmt->bind_param($types, ...$params); $stmt->execute(); $result = $stmt->get_result(); $stmt->close(); return $result; } /** * Execute an INSERT/UPDATE/DELETE and return affected rows * * @param string $sql SQL query with placeholders * @param string $types Parameter types * @param array $params Parameters to bind * @return int Affected rows (-1 on failure) */ public static function execute(string $sql, string $types = '', array $params = []): int { $conn = self::getConnection(); $stmt = $conn->prepare($sql); if (!$stmt) { throw new Exception("Query preparation failed: " . $conn->error); } if (!empty($types) && !empty($params)) { $stmt->bind_param($types, ...$params); } if ($stmt->execute()) { $affected = $stmt->affected_rows; $stmt->close(); return $affected; } $error = $stmt->error; $stmt->close(); throw new Exception("Query execution failed: " . $error); } /** * Get the last insert ID * * @return int Last insert ID */ public static function lastInsertId(): int { return self::getConnection()->insert_id; } // escape() removed — use prepared statements with bind_param() instead }