2026年7月12日
2026年7月12日
PHPのPDOでデータベースに接続する方法|接続設定・プリペアドステートメント・エラー対処
PHPからMySQL/MariaDBに接続するなら、PDOを使い「DSNに文字コードを含める」「例外モードでエラーを捕まえる」「値はプレースホルダで渡す」の3点を守るのが基本です。この記事では、接続コードの書き方と各オプションの意味、プリペアドステートメントによる安全なSELECT・INSERT、トランザクション、そして接続できないときのエラーメッセージ別の対処までを説明します。最後に、WordPressの中ではPDOではなく $wpdb を使うべき理由も触れます。
PDOとは何か
PDO(PHP Data Objects)は、PHPに標準で付いているデータベース接続の共通インターフェースです。MySQL・PostgreSQL・SQLiteなど、ドライバーを差し替えるだけで同じ書き方で操作できます。MySQL専用の mysqli もありますが、PDOは名前付きプレースホルダ(:name)が使えるなど書きやすく、新規のコードではPDOが選ばれることが多くなっています。
使うには、PHPに pdo_mysql 拡張が入っている必要があります。次のコマンドで確認できます。
php -m | grep -i pdo
PDO と pdo_mysql が表示されれば準備はできています。CLIとWebサーバー(PHP-FPMなど)で読み込むphp.iniが別のことがあるため、Web側は phpinfo() で確認してください。確認後は phpinfo() を置いたファイルを削除します。
接続の基本コード
接続情報はコードに直接書かず、Webから見えない場所の設定ファイルや環境変数から読み込みます。
<?php
// config.php はドキュメントルートの外に置く
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=app_db;charset=utf8mb4';
$user = getenv('DB_USER') ?: 'app_user';
$pass = getenv('DB_PASS') ?: '';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
error_log('DB connection failed: ' . $e->getMessage());
http_response_code(500);
exit('データベースに接続できませんでした。');
}
それぞれの設定には理由があります。
- **
charset=utf8mb4** … DSNに文字コードを書かないと、サーバーの既定値で通信して文字化けすることがあります。絵文字を扱うならutf8ではなくutf8mb4を指定します。 - **
ERRMODE_EXCEPTION** … SQLの失敗を例外として投げます。PHP 8.0以降は既定値ですが、明示しておくと古い環境でも挙動が変わりません。 - **
FETCH_ASSOC** … 結果を連想配列で返します。既定では連番と列名の両方が入った配列になり、扱いにくくなります。 - **
EMULATE_PREPARES => false** … プレースホルダの処理をPHP側の疑似実装ではなく、MySQLサーバーのプリペアドステートメントに任せます。後述するLIMITの問題も起きにくくなります。
例外のメッセージには接続先などの内部情報が含まれることがあるため、画面には出さずログに書き、利用者には一般的な文言だけを返します。本番環境ではphp.iniの display_errors を無効にしておきます。
プリペアドステートメントで安全にクエリを実行する
利用者の入力をSQL文字列に連結すると、SQLインジェクションの原因になります。値はプレースホルダで渡し、SQLの構造と値を分けます。
悪い例と良い例
// 悪い例:入力をそのまま連結している
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
$rows = $pdo->query($sql)->fetchAll();
// 良い例:プレースホルダで値を渡す
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute([':email' => $_GET['email'] ?? '']);
$user = $stmt->fetch(); // 見つからなければ false
SELECT:取り出し方の使い分け
fetch()… 1行ずつ取り出す。該当なしならfalse。fetchAll()… 全行を配列で取り出す。件数が多いとメモリを使うので注意。fetchColumn()… 1列目の値だけを返す。COUNT(*)などに便利です。
$stmt = $pdo->prepare('SELECT COUNT(*) FROM posts WHERE status = ?');
$stmt->execute(['publish']);
$count = (int) $stmt->fetchColumn();
LIMITや数値の扱い
EMULATE_PREPARES が有効なまま execute(['10']) で LIMIT ? に値を渡すと、文字列として引用符付きで埋め込まれてSQLエラーになります。数値は bindValue() で型を指定するのが安全な書き方です。
$stmt = $pdo->prepare('SELECT id, title FROM posts ORDER BY id DESC LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', 20, PDO::PARAM_INT);
$stmt->bindValue(':offset', 40, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll();
IN句に複数の値を渡す
プレースホルダは1つにつき値1つです。配列を渡したいときは、個数分の ? を組み立てます。
$ids = array_map('intval', [3, 5, 8]);
$in = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, title FROM posts WHERE id IN ($in)");
$stmt->execute($ids);
テーブル名や列名は許可リストで
プレースホルダで渡せるのは「値」だけで、テーブル名・列名・ORDER BY の向きは渡せません。並び替えの列を利用者に選ばせる場合は、許可する値をコード側で決めておき、それ以外は既定値に戻します。
$allowed = ['id', 'title', 'created_at'];
$orderBy = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'id';
$stmt = $pdo->query("SELECT id, title FROM posts ORDER BY {$orderBy} DESC");
INSERT・UPDATEとトランザクション
追加した行のIDは lastInsertId() で取得できます。複数のテーブルをまとめて更新するときは、トランザクションで「全部成功するか、全部取り消すか」にします。テーブルのストレージエンジンがInnoDBであることが前提です。
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO orders (user_id, total) VALUES (?, ?)');
$stmt->execute([$userId, $total]);
$orderId = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare('UPDATE stocks SET quantity = quantity - ? WHERE item_id = ? AND quantity >= ?');
$stmt->execute([$qty, $itemId, $qty]);
if ($stmt->rowCount() === 0) {
throw new RuntimeException('在庫が足りません');
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log($e->getMessage());
}
rowCount() はUPDATEやDELETEで変更された行数を返します。SELECTの件数取得には使わず、COUNT(*) を使ってください。本番データを書き換える処理を初めて動かすときは、事前にバックアップを取り、ステージング環境で結果を確認します。
接続できないときのエラー別チェック
接続エラーは、メッセージに出ているコードでおおよその原因が分かります。よく見るものから順に挙げます。
**could not find driver**pdo_mysql 拡張が読み込まれていません。Web側の phpinfo() で確認し、php.iniで有効にするか、パッケージを入れてPHP-FPMやWebサーバーを再起動します。
**SQLSTATE[HY000] [2002] No such file or directory**host=localhost と書くと、TCPではなくUnixソケットで接続しようとし、ソケットのパスが見つからないときに出ます。host=127.0.0.1 に変えるか、unix_socket=/path/to/mysqld.sock をDSNに指定します。ソケットの場所は SHOW VARIABLES LIKE 'socket'; で確認できます。
**SQLSTATE[HY000] [2002] Connection refused / タイムアウト**MySQLが起動していない、ポートが違う、別サーバーへの接続をファイアウォールが止めている、のいずれかです。DBサーバーで systemctl status mysql(サービス名は環境で異なります)を確認し、別サーバーなら接続元から nc -zv ホスト 3306 などで疎通を確かめます。
**SQLSTATE[HY000] [1045] Access denied for user**ユーザー名・パスワード、または接続元ホストの許可が合っていません。MySQLのユーザーは 'app_user'@'localhost' と 'app_user'@'%' が別物として扱われる点に注意してください。
**SQLSTATE[HY000] [1049] Unknown database**データベース名の誤りです。大文字・小文字の区別はOSや設定によって変わります。
**[2054] The server requested authentication method unknown to the client**MySQL 8系の認証方式に古いPHPが対応していない場合に出ます。まずPHPを新しくすることを検討します。
WordPressの中ではPDOより$wpdbを使う
WordPressはデータベース接続に独自の $wpdb クラス(内部ではmysqli)を使っています。プラグインやテーマから同じデータベースに別途PDOで接続すると、接続が二重になるうえ、WordPressのキャッシュやフックとも連携しません。WordPressのテーブルを扱うなら $wpdb->prepare() を使い、値は %d・%s・%f、テーブル名などの識別子は %i(WordPress 6.2以降)で渡します。
global $wpdb;
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts} WHERE post_status = %s AND post_author = %d",
'publish',
get_current_user_id()
)
);
WordPressとは別の外部データベースを読む場合は、PDOを使うか、new wpdb( $user, $password, $name, $host ) で別インスタンスを作る方法があります。どちらでも接続情報は wp-config.php の定数や環境変数に置き、コードに直接書かないようにします。
PHPやMySQLのバージョンごとの細かな挙動は変わることがあります(2026年10月現在)。古い環境で動かす場合は、PHPマニュアルの該当バージョンの記述も確認してください。
まとめ
- DSNに
charset=utf8mb4を入れ、ERRMODE_EXCEPTION・FETCH_ASSOC・EMULATE_PREPARES => falseを設定する - 例外メッセージは画面に出さずログへ。接続情報はドキュメントルートの外か環境変数に置く
- 値はプレースホルダで渡し、LIMITなどの数値は
bindValue(..., PDO::PARAM_INT)を使う - テーブル名・列名はプレースホルダで渡せないため、許可リストで制限する
- 接続エラーは
2002・1045・1049・could not find driverなどのコードから原因を絞る - WordPress内では
$wpdb->prepare()を使い、PDOで二重に接続しない