quote((string) $connection['host']) . ';port=' . min(max((int) $connection['port'], 1), 65535) . ';dbname=' . $this->quote((string) $connection['database']) . ';sslmode=require;connect_timeout=' . $timeout . ";options='-c default_transaction_read_only=on -c statement_timeout=" . $queryTimeout . "'"; try { // One short-lived connection, TLS and read-only enforced at session startup. $pdo = $this->connect($dsn, (string) $connection['username'], (string) $connection['password']); $statement = $pdo->query('SELECT 1'); if ($statement === false || (int) $statement->fetchColumn() !== 1) { throw new ServiceUnavailableHttpException(30, 'PostgreSQL 只读测试未返回预期结果。'); } $statement->closeCursor(); } catch (PDOException) { // Do not expose raw driver errors, DSNs or credentials in logs/session flashes. throw new ServiceUnavailableHttpException(30, 'PostgreSQL 连接失败。请检查账号、密码、数据库访问权限,以及服务器网络和 SSL 配置。'); } } protected function connect(string $dsn, string $username, #[SensitiveParameter] string $password): PDO { if (! in_array('pgsql', PDO::getAvailableDrivers(), true)) { throw new ServiceUnavailableHttpException(30, '应用缺少 PostgreSQL 驱动,请部署包含 pdo_pgsql 的新版应用镜像。'); } return new PDO($dsn, $username, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_PERSISTENT => false, ]); } private function quote(string $value): string { // PDO replaces all DSN semicolons with spaces; reject rather than alter database names. if (preg_match('/[;\x00-\x1F]/', $value)) { throw new ServiceUnavailableHttpException(30, '主机或数据库名称包含不支持的字符。'); } return "'" . str_replace(['\\', "'"], ['\\\\', "\\'"], $value) . "'"; } }