在通过入门指南建立可用的连接后,请使用本指南通过任一 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 驱动程序的安全注意事项 |