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で二重に接続しない

お気軽にご相談ください

お見積りへ お問い合わせへ
▲