當你從 開始使用 建立起可用連線後,使用本指南撰寫、提交並讀取一列資料,並使用其中任一種 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。 範例中保持資料表和欄位名稱不變:值參數無法取代識別符。 預備語句也允許你以不同值再次執行相同的語句。 欲了解更多資訊,請參閱 「比較查詢執行函數」。
| 選擇 | Reason |
|---|---|
| 明確交易方法 | 範例將寫入視為必須提交或回滾的單元。 SQLSRV 使用 sqlsrv_begin_transaction、 sqlsrv_commit、 sqlsrv_rollback和 ;PDO 使用自己的交易方法。 |
| SQLSRV parameter descriptors | 每個描述符指定方向、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.phpworkflow-pdo.php他們,如果你不再需要。 完成後,從測試程序中清除憑證環境變數。 這兩個程式都不會留下永久的資料表可供移除。
這些攻略針對 SQL Server。 對於 Azure SQL Database、Azure SQL 受控執行個體、Azure Synapse Analytics、Fabric 中的 SQL 資料庫,以及 Fabric 中的 Warehouse,請先檢查目標的認證與 T-SQL 支援。 這兩個 Fabric 產品都需要 Microsoft Entra ID 認證,而 Warehouse 則需要停用多個主動結果集(MARS)。 這些 SQL Server 程式不提供服務專屬的交易或臨時資料表支援。
選擇你的下一個任務
使用符合所需行為的文章來擴充工作流程。
| 任務 | Guidance |
|---|---|
| 選擇認證或管理連線 | 連線到伺服器。 |
| 以不同值重複使用預備敘述 | 比較查詢執行函數。 |
| 選擇 PDO 直接執行或預備語句執行 | PDO_SQLSRV驅動程式中執行直接語句與預備語句執行。 |
| 取得值並選擇結果形狀 | 正在取回資料。 |
| 捲動或緩存結果 | 指定游標類型並選擇資料列。 |
| 變更應用程式資料 | 更新資料。 |
| 控制 PHP 與 SQL 型別轉換 | 資料型態轉換。 |
| 診斷故障並啟用驅動程式日誌 | 處理錯誤與警告,以及 日誌記錄。 |
| 保護敏感欄位 | 使用 Always Encrypted 搭配 Microsoft Drivers for PHP for SQL Server。 |
| 強化正式環境中的應用程式 | Microsoft PHP for SQL Server 驅動程式的安全考量。 |