「 Getting Started」から接続が動作したら、このガイドを使ってどちらかの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 を挿入します。 影響を受ける行が正確に1行であることを要求し、トランザクションをコミットし、返された値と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 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型 | fetch属性とパラメータディスクリプタを確認してください。 厳密なアサーションを、違いを隠すために緩い比較に置き換えないでください。 |
| 書き込みまたはロールバックの失敗 | SQLSRVのエラーを即座にキャプチャするか、PDO例外を即座にキャプチャしてください。 ロールバックの失敗も報告してください。ロールバックが失敗した場合は、書き込みが元に戻されたと主張しないでください。 |
| 接続切断後のクリーンアップ失敗 | プログラムはサーバー側のドロップを確認できません。 PHPハンドルを解放しても、基礎となるプールされた物理接続がすぐに閉じられたとは限りません。 |
PHP拡張はバインディングとフェッチAPIを提供します。 ODBCはサーバーと通信し、接続プーリングを管理します。 SQL Serverは一時テーブルのスコープ、トランザクション、制約、コミットされたデータを制御します。 物理接続のクローズによるクリーンアップに頼るのではなく、テーブルを明示的にドロップしましょう。
エクササイズが終わったら、もう必要ない workflow-sqlsrv.php と workflow-pdo.php を削除してください。 テストプロセスが終了したら、認証情報環境変数をテストプロセスから削除してください。 どちらのプログラムも、削除するための恒久的なテーブルを残しません。
これらのウォークスルーはSQL Serverを対象としています。 Azure SQL Database、Azure SQL Managed Instance、Azure Synapse Analytics、FabricのSQL Database、Warehouse in Fabricについては、まずターゲットの認証とT-SQLのサポートを確認してください。 両方の Fabric 製品には Microsoft Entra ID 認証が必要であり、Warehouse では複数のアクティブ結果セット (MARS) を無効にする必要があります。 これらのSQL Serverプログラムは、サービス固有のトランザクションや一時テーブルのサポートを確立していません。
次のタスクを選択する
必要な動作の記事を追加してワークフローを拡張しましょう。
| Task | Guidance |
|---|---|
| 認証を選ぶか、接続を管理するかを選びます | サーバーに接続中。 |
| 異なる値でプリペアドステートメントを再利用する | クエリ実行関数を比較してください。 |
| PDOの直接実行かプリペアド実行かを選択 | PDO_SQLSRV ドライバーでの直接ステートメント実行と準備済みステートメント実行 |
| 値の取得と結果形状の選択 | データを取得中。 |
| 結果をスクロールまたはバッファリングします | カーソルの種類を指定し、行を選択すること。 |
| アプリケーションデータを変更する | データの更新. |
| PHP および SQL の型変換を制御する | データ型の変換。 |
| 障害の診断とドライバーログの有効化 | エラーや警告の処理、ログの記録。 |
| 機密性の高いカラムを保護 | SQL Server 用 Microsoft PHP ドライバーで Always Encrypted を使用する。 |
| 本番環境のアプリケーションを強化する | Microsoft Drivers for PHP for SQL Serverのセキュリティ上の考慮事項 |