Files
kaffeekasse-saas/lib/sqlsrv_mysql_compat.php
T
2026-07-11 22:24:22 +02:00

246 lines
7.0 KiB
PHP

<?php
if (!defined('SQLSRV_FETCH_ASSOC')) {
define('SQLSRV_FETCH_ASSOC', 2);
}
if (!defined('SQLSRV_FETCH_NUMERIC')) {
define('SQLSRV_FETCH_NUMERIC', 1);
}
final class MysqlSqlsrvConnection
{
public PDO $pdo;
public ?array $lastError = null;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
}
final class MysqlSqlsrvStatement
{
/** @var array<int, array<string, mixed>> */
public array $rows;
public int $index = 0;
public int $affectedRows = 0;
/**
* @param array<int, array<string, mixed>> $rows
*/
public function __construct(array $rows = [], int $affectedRows = 0)
{
$this->rows = $rows;
$this->affectedRows = $affectedRows;
}
}
function sqlsrv_connect($serverName, array $connectionOptions = [])
{
$database = getenv('DB_NAME') ?: ($connectionOptions['Database'] ?? '');
$user = getenv('DB_USER') ?: ($connectionOptions['Uid'] ?? '');
$password = getenv('DB_PASS') ?: ($connectionOptions['PWD'] ?? '');
$host = getenv('DB_HOST') ?: (string)$serverName;
$port = getenv('DB_PORT') ?: '3306';
try {
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database);
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => true,
]);
return new MysqlSqlsrvConnection($pdo);
} catch (Throwable $e) {
$GLOBALS['__sqlsrv_mysql_last_error'] = [[
'SQLSTATE' => $e->getCode(),
'message' => $e->getMessage(),
]];
return false;
}
}
function sqlsrv_query($conn, string $sql, array $params = [], array $options = [])
{
if (!$conn instanceof MysqlSqlsrvConnection) {
$GLOBALS['__sqlsrv_mysql_last_error'] = [[
'SQLSTATE' => 'HY000',
'message' => 'Invalid MySQL compatibility connection.',
]];
return false;
}
$translatedSql = sqlsrv_mysql_translate_query($sql);
try {
$stmt = $conn->pdo->prepare($translatedSql);
$stmt->execute(sqlsrv_mysql_normalize_params($translatedSql, $params));
if ($stmt->columnCount() > 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$row) {
$row = sqlsrv_mysql_normalize_row($row);
}
unset($row);
return new MysqlSqlsrvStatement($rows, $stmt->rowCount());
}
return new MysqlSqlsrvStatement([], $stmt->rowCount());
} catch (Throwable $e) {
$conn->lastError = [[
'SQLSTATE' => $e->getCode(),
'message' => $e->getMessage(),
'sql' => $translatedSql,
]];
$GLOBALS['__sqlsrv_mysql_last_error'] = $conn->lastError;
return false;
}
}
function sqlsrv_fetch_array($stmt, int $fetchType = SQLSRV_FETCH_ASSOC)
{
if (!$stmt instanceof MysqlSqlsrvStatement) {
return null;
}
if (!array_key_exists($stmt->index, $stmt->rows)) {
return null;
}
$row = $stmt->rows[$stmt->index++];
if ($fetchType === SQLSRV_FETCH_NUMERIC) {
return array_values($row);
}
return $row;
}
function sqlsrv_num_rows($stmt): int
{
if (!$stmt instanceof MysqlSqlsrvStatement) {
return 0;
}
return count($stmt->rows);
}
function sqlsrv_errors()
{
return $GLOBALS['__sqlsrv_mysql_last_error'] ?? null;
}
function sqlsrv_close($conn): bool
{
return true;
}
function sqlsrv_begin_transaction($conn): bool
{
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->beginTransaction();
}
function sqlsrv_commit($conn): bool
{
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->commit();
}
function sqlsrv_rollback($conn): bool
{
return $conn instanceof MysqlSqlsrvConnection && $conn->pdo->rollBack();
}
function sqlsrv_mysql_translate_query(string $sql): string
{
$query = str_replace(["\r\n", "\r"], "\n", $sql);
$query = preg_replace('/\bdbo\./i', '', $query);
$query = preg_replace('/\[([A-Za-z0-9_]+)\]/', '`$1`', $query);
$query = preg_replace('/FORMAT\(([^,]+),\s*\'yyyy\'\)\s*=\s*FORMAT\(GETDATE\(\),\s*\'yyyy\'\)/i', 'YEAR($1) = YEAR(NOW())', $query);
$query = preg_replace('/CAST\(GETDATE\(\)\s+AS\s+DATE\)/i', 'CURDATE()', $query);
$query = preg_replace('/GETDATE\(\)/i', 'NOW()', $query);
$query = preg_replace('/SYSDATETIME\(\)/i', 'NOW()', $query);
$query = preg_replace('/CONVERT\(VARCHAR,\s*([^)]+),\s*23\)/i', 'DATE($1)', $query);
$query = preg_replace(
'/DATEADD\(DAY,\s*(-?\d+),\s*\(SELECT\s+MAX\(Datum\)\s+FROM\s+kl_Kaffeeverbrauch\s+WHERE\s+Datum\s+<\s+CURDATE\(\)\)\)/i',
'DATE_ADD((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch WHERE Datum < CURDATE()), INTERVAL $1 DAY)',
$query
);
$query = preg_replace(
'/DATEADD\(DAY,\s*(-?\d+),\s*\(SELECT\s+MAX\(Datum\)\s+FROM\s+kl_Kaffeeverbrauch\s*\)\)/i',
'DATE_ADD((SELECT MAX(Datum) FROM kl_Kaffeeverbrauch), INTERVAL $1 DAY)',
$query
);
$query = preg_replace('/DATEADD\(DAY,\s*(-?\d+),\s*NOW\(\)\)/i', 'DATE_ADD(NOW(), INTERVAL $1 DAY)', $query);
$limit = null;
$query = preg_replace_callback('/^\s*SELECT\s+TOP\s+\(?\s*(\d+)\s*\)?\s+/i', function (array $matches) use (&$limit): string {
$limit = (int)$matches[1];
return 'SELECT ';
}, $query);
if ($limit !== null && !preg_match('/\bLIMIT\s+\d+\s*$/i', trim($query))) {
$query = rtrim($query, " \t\n\r\0\x0B;") . ' LIMIT ' . $limit;
}
return $query;
}
/**
* PDO is stricter than the SQL Server driver used by the legacy app. Some
* legacy calls pass unused parameters, and one CSV lookup passes one value for
* two equivalent placeholders. Normalize this only in the dev adapter.
*
* @param array<int, mixed> $params
* @return array<int, mixed>
*/
function sqlsrv_mysql_normalize_params(string $sql, array $params): array
{
$normalized = array_values($params);
$placeholderCount = substr_count($sql, '?');
if ($placeholderCount === 0) {
return [];
}
if (count($normalized) > $placeholderCount) {
return array_slice($normalized, 0, $placeholderCount);
}
if (count($normalized) < $placeholderCount && count($normalized) > 0) {
$last = $normalized[count($normalized) - 1];
while (count($normalized) < $placeholderCount) {
$normalized[] = $last;
}
}
return $normalized;
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
function sqlsrv_mysql_normalize_row(array $row): array
{
foreach ($row as $key => $value) {
if ($value === null || $value instanceof DateTimeInterface) {
continue;
}
if (is_string($value) && preg_match('/(datum|date|gueltig_bis|valid_)/i', (string)$key)) {
try {
$row[$key] = new DateTime($value);
} catch (Throwable $e) {
$row[$key] = $value;
}
}
}
return $row;
}