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。 範例中保持資料表和欄位名稱不變:值參數無法取代識別符。 預備語句也允許你以不同值再次執行相同的語句。 欲了解更多資訊,請參閱 「比較查詢執行函數」。

選擇 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 驅動程式的安全考量。