Microsoft PHP 驱动 for SQL Server 的编程指南

下载 PHP 驱动程序

在通过入门指南建立可用的连接后,请使用本指南通过任一 PHP API 写入、提交并读取一行数据。 每个程序都包含自己的设置和清理工作。 利用任务链接扩展工作流程,而不是把选项从一个API复制到另一个API。

准备测试连接

使用现有的 SQL Server 测试数据库和一个可以连接并创建本地临时表的登录名。 示例中使用了SQL认证。 有关支持的认证选项和无密码 Azure 引导,请参见“连接到服务器”。

加载你选择的扩展,并在 PHP 进程的环境中设置 SQL_SERVER、SQL_DATABASE、SQL_USER 和 SQL_PASSWORD。 SQL_SERVER 可以包含显式端口,如 <server>,1433。 服务器必须提供客户端信任且对该端点有效的证书。 请将密码置于程序和源代码控制之外。

将实际的数据库名称、用户名和密码存储在环境变量中。 每个程序都会针对 PHP 驱动程序的 ODBC 风格连接处理要求,将右花括号写成两个,并用花括号将这些值括起来一次。 转义保留连接值中的特殊字符;这不是查询参数化。 更多信息请参见“连接服务器”。

Transact-SQL(T-SQL)语句创建 #PhpGuide了一个临时表,仅对创建它的连接可见。 这些程序不会创建或更改永久应用程序表。 在整个工作流程中保持相同的连接。

写入并读取一行

程序通过参数插入整数、UTF-8 文本和 SQL NULL 。 它们要求恰好影响一行,提交事务,并验证返回的值和 PHP 类型。 最终查询还会检查是否没有事务仍处于打开状态。

SQLSRV

将此完整程序保存为 workflow-sqlsrv.php,并运行 php workflow-sqlsrv.php。

<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$conn = sqlsrv_connect($config['SQL_SERVER'], [
    'Database' => $config['SQL_DATABASE'],
    'UID' => $config['SQL_USER'],
    'PWD' => $config['SQL_PASSWORD'],
    'Encrypt' => true,
    'TrustServerCertificate' => false,
    'LoginTimeout' => 30,
    'CharacterSet' => 'UTF-8',
]);
if ($conn === false) {
    throw new RuntimeException(print_r(sqlsrv_errors(), true));
}
$stmt = null;
$inTransaction = false;
$failure = null;
try {
    $stmt = sqlsrv_query($conn, 'CREATE TABLE #PhpGuide '
        . '(Id int PRIMARY KEY, Title nvarchar(80), OptionalCount int NULL)');
    if ($stmt === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    sqlsrv_free_stmt($stmt);
    $stmt = null;
    if (!sqlsrv_begin_transaction($conn)) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    $inTransaction = true;
    $id = 1;
    $title = "Caf\u{00E9}";
    $optionalCount = null;
    $params = [
        [&$id, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
        [&$title, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('UTF-8'), SQLSRV_SQLTYPE_NVARCHAR(80)],
        [&$optionalCount, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_INT, SQLSRV_SQLTYPE_INT],
    ];
    $stmt = sqlsrv_prepare($conn,
        'INSERT INTO #PhpGuide (Id, Title, OptionalCount) VALUES (?, ?, ?)', $params);
    if ($stmt === false || !sqlsrv_execute($stmt)) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    $affected = sqlsrv_rows_affected($stmt);
    if ($affected === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    if ($affected !== 1) {
        throw new RuntimeException('Expected exactly one inserted row.');
    }
    sqlsrv_free_stmt($stmt);
    $stmt = null;
    if (!sqlsrv_commit($conn)) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    $inTransaction = false;
    $stmt = sqlsrv_query($conn,
        'SELECT Id, Title, OptionalCount, @@TRANCOUNT AS TransactionCount FROM #PhpGuide');
    if ($stmt === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
    if ($row === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    if ($row !== ['Id' => 1, 'Title' => $title, 'OptionalCount' => null, 'TransactionCount' => 0]) {
        throw new RuntimeException('Unexpected committed values, types, or transaction state.');
    }
    $end = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
    if ($end === false) {
        throw new RuntimeException(print_r(sqlsrv_errors(), true));
    }
    if ($end !== null) {
        throw new RuntimeException('Expected exactly one result row.');
    }
} catch (Throwable $error) {
    $failure = $error;
    if ($inTransaction && !sqlsrv_rollback($conn)) {
        $failure = new RuntimeException(
            'Rollback failed: ' . print_r(sqlsrv_errors(), true), 0, $error);
    }
    throw $failure;
} finally {
    if (is_resource($stmt)) {
        sqlsrv_free_stmt($stmt);
    }
    try {
        $cleanup = sqlsrv_query($conn, 'DROP TABLE IF EXISTS #PhpGuide');
        if ($cleanup === false) {
            throw new RuntimeException(
                'Cleanup failed: ' . print_r(sqlsrv_errors(), true), 0, $failure);
        }
        sqlsrv_free_stmt($cleanup);
    } finally {
        sqlsrv_close($conn);
    }
}
echo "Inserted 1 row; read committed UTF-8 text and null; no open transaction.", PHP_EOL;

PDO_SQLSRV

将这个独立程序保存为 workflow-pdo.php,并运行 php workflow-pdo.php。

<?php
$config = [];
foreach (['SQL_SERVER', 'SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $value = getenv($name);
    if ($value === false || $value === '') {
        throw new RuntimeException("Set $name before running this example.");
    }
    $config[$name] = $value;
}
foreach (['SQL_DATABASE', 'SQL_USER', 'SQL_PASSWORD'] as $name) {
    $config[$name] = '{' . str_replace('}', '}}', $config[$name]) . '}';
}
$server = $config['SQL_SERVER'];
$database = $config['SQL_DATABASE'];
$pdo = new PDO(
    "sqlsrv:Server=$server;Database=$database;Encrypt=true;"
    . "TrustServerCertificate=false;LoginTimeout=30",
    $config['SQL_USER'],
    $config['SQL_PASSWORD'],
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8,
        PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
    ]
);
$stmt = null;
$failure = null;
try {
    $pdo->exec('CREATE TABLE #PhpGuide '
        . '(Id int PRIMARY KEY, Title nvarchar(80), OptionalCount int NULL)');
    $pdo->beginTransaction();
    $title = "Caf\u{00E9}";
    $stmt = $pdo->prepare(
        'INSERT INTO #PhpGuide (Id, Title, OptionalCount) VALUES (?, ?, ?)');
    $stmt->bindValue(1, 1, PDO::PARAM_INT);
    $stmt->bindValue(2, $title, PDO::PARAM_STR);
    $stmt->bindValue(3, null, PDO::PARAM_NULL);
    $stmt->execute();
    if ($stmt->rowCount() !== 1) {
        throw new RuntimeException('Expected exactly one inserted row.');
    }
    $stmt = null;
    $pdo->commit();
    $stmt = $pdo->query(
        'SELECT Id, Title, OptionalCount, @@TRANCOUNT AS TransactionCount FROM #PhpGuide');
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if ($row !== ['Id' => 1, 'Title' => $title, 'OptionalCount' => null, 'TransactionCount' => 0]) {
        throw new RuntimeException('Unexpected committed values, types, or transaction state.');
    }
    if ($stmt->fetch(PDO::FETCH_ASSOC) !== false || $pdo->inTransaction()) {
        throw new RuntimeException('Expected one result row and no open transaction.');
    }
} catch (Throwable $error) {
    $failure = $error;
    if ($pdo->inTransaction()) {
        try {
            $pdo->rollBack();
        } catch (PDOException $rollbackError) {
            $failure = new RuntimeException(
                'Rollback failed: ' . $rollbackError->getMessage(), 0, $error);
        }
    }
    throw $failure;
} finally {
    $stmt = null;
    try {
        $pdo->exec('DROP TABLE IF EXISTS #PhpGuide');
    } catch (PDOException $cleanupError) {
        throw new RuntimeException(
            'Cleanup failed: ' . $cleanupError->getMessage(), 0, $failure ?? $cleanupError);
    } finally {
        $pdo = null;
    }
}
echo "Inserted 1 row; read committed UTF-8 text and null; no open transaction.", PHP_EOL;

任一项目的预期输出:

Inserted 1 row; read committed UTF-8 text and null; no open transaction.

输出仅在检查和清理成功后才会出现。 连接、写入、取回、回滚或表清理失败会触发异常。

了解这些选择

使用 ? 占位符表示值,而不是将输入串接到T-SQL中。 示例保持了表和列名的固定:值参数不能替代标识符。 预编译语句还允许你使用不同的值再次执行同一条语句。 更多信息请参见 “比较查询执行函数”。

选项 原因
显式事务方法 示例将写入视为必须提交或回滚的单元。 SQLSRV 使用 sqlsrv_begin_transaction、 sqlsrv_commit和 sqlsrv_rollback;PDO 使用自己的事务方法。
SQLSRV 参数描述符 每个描述符指定方向、PHP 类型和 SQL 类型。 引用保留输入变量,包括保存 null 的变量。
PDO bindValue 语句以显式的 PDO 参数类型接收每个当前值。 bindParam 当后续执行必须通过引用读取变量时,该方法适用。
UTF-8 加 nvarchar PHP 提供 UTF-8 字节;扩展和 ODBC 驱动将它们转换为 SQL Unicode 列。 获取的文本必须与原始的PHP字符串字节逐字节匹配。
为 PDO 显式启用数值获取 结果比较预期是 PHP 整数。 没有PDO_SQLSRV属性时,这些SQL整数结果使用驱动程序默认的字符串表示。
命名获取键 SQLSRV_FETCH_ASSOC 并 PDO::FETCH_ASSOC 返回按列名索引的数组。 API 常量不能互换。
PDO::exec 用于表格设置 它直接执行表创建语句。 这种方法避免在服务器准备语句的作用域内创建临时表。
仅前进结果 这些程序对每一行读取一次。 它们不会请求缓冲结果,也不会用SELECT行计数作为成功获取数据的证明。

当数组获取到达末尾时,SQLSRV会返回false;发生获取错误时,则返回null。 PDO 的获取操作在结束时返回 false;在配置了异常模式的情况下,会抛出数据库获取错误。 在这两个API中,字段 OptionalCount 本身都包含PHP null. 这个字段值并不是结果结束的标志。

写入提交在验证查询之前。 如果后续读取失败,之前提交的写入无法回滚。 在此示例中,该行仅属于一个临时表,该临时表会由清理操作删除。 在有永久表的应用中,准确报告失败,不要在提交结果不确定后盲目重复插入。

处理失败并进行清理

catch 块会回滚活动事务,并重新抛出该失败。 该 finally 块释放语句,丢弃临时表,并释放连接。 清理会在成功路径和失败路径上运行。 如果回滚或表清理也失败,程序会将原始异常保留为前一个异常。

对于命令行练习,未捕获的异常会使 PHP 停止运行,并返回非零退出代码和一条错误消息。 在网页应用中,用处理请求的代码捕捉它。 在应用日志中记录诊断前,先移除敏感值,并返回相应的错误响应。 不要向浏览器暴露连接详情、凭证或原始数据库错误。

Failure 需要检查的事项
缺少扩展功能或驱动程序 确认 PHP 进程加载了预期的扩展,并且运行时和配置相匹配。 一个可用的命令行PHP配置并不能证明你的网页服务器使用了它。
连接或证书故障 在更改查询代码之前,请检查终结点、身份验证、权限和客户端证书信任。 不要将禁用证书验证作为生产环境的补救措施。
找不到临时表 设置、写入和读取都在同一连接上进行。 对于 PDO 设置,请保留示例中演示的 exec 路径,而不要替换为默认的预编译 query 调用。
意外值或PHP类型 检查获取属性和参数描述符。 不要用宽泛的比较替换严格断言来掩盖差异。
写入或回滚失败 立即捕获 SQLSRV 的错误,或捕获 PDO 异常。 回滚失败也要报告;如果回滚失败,不要声称写入被撤销了。
连接丢失后的清理失败 该程序无法确认服务器端的删除。 释放 PHP 句柄并不代表底层的池化物理连接立即关闭。

PHP 扩展提供了绑定和获取 API。 ODBC 与服务器通信并管理连接池。 SQL Server 控制临时表的作用域、事务、约束和已提交数据。 显式删除该表,而不是依赖物理连接关闭来执行清理。

练习结束后,如果你不再需要它们,请删除workflow-sqlsrv.php和workflow-pdo.php。 完成后,清除测试进程中的凭证环境变量。 这两个程序都不会留下需要移除的永久表。

这些演练针对 SQL Server。 对于 Azure SQL 数据库、Azure SQL 托管实例、Azure Synapse Analytics、Fabric 中的 SQL 数据库和 Warehouse in Fabric,首先检查目标的认证和 T-SQL 支持。 这两款 Fabric 产品都需要 Microsoft Entra ID 认证,且仓库需要禁用多个主动结果集(MARS)。 这些 SQL Server 程序不会建立特定于服务的事务或临时表支持。

选择下一个任务

请根据你需要的行为,按照相应文章中的说明来扩展该工作流。

任务 Guidance
选择认证或管理连接 连接服务器。
使用不同的值重用预编译语句 比较查询执行函数。
选择PDO直接执行或预处理执行 PDO_SQLSRV驱动中的直接语句执行和预备语句执行。
获取值并选择结果形态 正在检索数据。
滚动或缓冲结果 指定光标类型并选择行。
更改应用数据 更新数据。
控制PHP和SQL类型转换 数据类型转换。
诊断故障并启用驱动程序日志 处理错误和警告 以及 日志活动。
保护敏感列 在 PHP 版 Microsoft Drivers for SQL Server 中使用 Always Encrypted。
加固生产应用 关于 Microsoft PHP for SQL Server 驱动程序的安全注意事项