pdo = $pdo; } } final class MysqlSqlsrvStatement { /** @var array> */ public array $rows; public int $index = 0; public int $affectedRows = 0; /** * @param array> $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 $params * @return array */ 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 $row * @return array */ 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; }