Lint / PHP (phpcs PSR-12) (push) Successful in 20s
Lint / JS (eslint) (push) Successful in 7s
Lint / PHP requirements (version + extensions) (push) Successful in 19s
Lint / PHP (phpcs PSR-12) (pull_request) Successful in 29s
Lint / JS (eslint) (pull_request) Successful in 14s
Lint / PHP requirements (version + extensions) (pull_request) Successful in 39s
Security / PHP Security (semgrep) (push) Successful in 1m15s
Security / PHP Security (semgrep) (pull_request) Successful in 1m23s
Lint / Deploy (push) Successful in 2s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (pull_request) Has been skipped
Lint / Notify on failure (pull_request) Has been skipped
The hosts were upgraded to PHP 8.4, where mysqli::ping() is deprecated
(auto-reconnect was removed in 8.2). Database::getConnection() called it on
every reused connection, and api/ticket_dependencies.php's custom error
handler treated the deprecation as a fatal 500 ('A server error occurred'),
breaking the ticket Dependencies tab.
- Database.php: remove the redundant ping()/reconnect check (connection is
request-scoped; no liveness check needed on PHP 8.2+).
- ticket_dependencies.php: only abort on genuine errors; log notices/
warnings/deprecations and continue, so a future deprecation can't 500 it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
202 lines
5.9 KiB
PHP
202 lines
5.9 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Database Connection Factory
|
|
*
|
|
* Centralizes database connection creation and management.
|
|
* Provides a singleton connection for the request lifecycle.
|
|
*/
|
|
class Database
|
|
{
|
|
private static ?mysqli $connection = null;
|
|
|
|
/**
|
|
* Get database connection (singleton pattern)
|
|
*
|
|
* @return mysqli Database connection
|
|
* @throws Exception If connection fails
|
|
*/
|
|
public static function getConnection(): mysqli
|
|
{
|
|
if (self::$connection === null) {
|
|
self::$connection = self::createConnection();
|
|
}
|
|
|
|
// Note: no ping()/reconnect check — mysqli auto-reconnect was removed in
|
|
// PHP 8.2 and mysqli::ping() is deprecated in 8.4. The connection is
|
|
// request-scoped and short-lived, so a liveness check is unnecessary.
|
|
return self::$connection;
|
|
}
|
|
|
|
/**
|
|
* Create a new database connection
|
|
*
|
|
* @return mysqli Database connection
|
|
* @throws Exception If connection fails
|
|
*/
|
|
private static function createConnection(): mysqli
|
|
{
|
|
// Ensure config is loaded
|
|
if (!isset($GLOBALS['config'])) {
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
|
}
|
|
|
|
$conn = new mysqli(
|
|
$GLOBALS['config']['DB_HOST'],
|
|
$GLOBALS['config']['DB_USER'],
|
|
$GLOBALS['config']['DB_PASS'],
|
|
$GLOBALS['config']['DB_NAME']
|
|
);
|
|
|
|
if ($conn->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
|
|
}
|