2026年9月18日
2026年9月18日
WordPressのPHPからPDOでデータベースに直接アクセスする安全な方法
WordPressの中でデータベースを扱うときは、まず $wpdb を使うのが基本で、PDOが向いているのは「WordPressとは別のデータベースを読む」「トランザクションや型の扱いをPDOで統一したい」といった限られた場面です。この記事では、PDOを使うべきかの判断、接続情報の安全な置き場所、DB_HOST の書式の違いへの対応、プリペアドステートメントの正しい使い方、エラー処理、WordPressのテーブルを直接書き換えてはいけない理由までを説明します。
コードは子テーマの functions.php ではなく、テーマを変えても動き続ける mu-plugin(wp-content/mu-plugins/ に置くPHPファイル)か独自プラグインに置くことをおすすめします。データを書き換える処理を試す前にはバックアップを取り、ステージング環境で動作を確認してください。
まず$wpdbで済まないかを考える
$wpdb はWordPressに組み込まれたデータベース操作クラスで、次の利点があります。
$wpdb->prepare()によるプレースホルダー(%d、%s、%f、WordPress 6.2以降は識別子用の%i)が使える- テーブル接頭辞を
$wpdb->prefixで取得でき、マルチサイトにも対応する - Query Monitorなどのデバッグ用プラグインでクエリを確認できる
- WordPressと同じ接続を使うため、接続数が増えない
WordPressとは別のMySQL/MariaDBデータベースに接続したい場合も、new wpdb( $user, $password, $dbname, $host ) で別の接続を作れます。PDOが本当に必要になるのは、MySQL系以外のデータベース(SQLiteなど)に接続したい場合、既存のPDOベースのライブラリを組み込む場合、例外ベースのエラー処理やトランザクションをPDOのAPIで書きたい場合などです。
もう一つの前提として、WordPress本体のテーブル(wp_posts、wp_options、wp_users など)への書き込みはPDOで行わないでください。wp_insert_post() や update_option() などの関数を通さずに書き換えると、オブジェクトキャッシュが古いまま残る、フックが動かずプラグインが変更を検知できない、シリアライズされた値を壊す、といった問題が起きます。PDOは「WordPress外のデータ」か「自作の独立したテーブルの読み取り」に限定するのが安全です。
手順1:PDOとドライバーが使えるか確認する
サーバーでPDOとMySQL用ドライバーが有効になっているかを確認します。
php -m | grep -i pdo
PDO と pdo_mysql が表示されれば使えます。コマンドラインのPHPとWebサーバーのPHPで設定が異なる場合があるため、管理画面の「ツール」→「サイトヘルス」→「情報」タブにあるサーバーの項目や、ホスティング会社の管理画面でも確認してください(2026年10月現在)。項目の名称はWordPressのバージョンにより異なる場合があります。
手順2:接続情報はwp-config.phpの定数で管理する
接続情報をプラグインのPHPファイルに直接書くと、Gitリポジトリへの誤ったコミットや、ファイルの差し替え時の流出につながります。WordPressの外のデータベースに接続する場合は、wp-config.php に専用の定数を定義します。
// wp-config.php(「編集が必要なのはここまでです」の行より上に書く)
define( 'EXT_DB_HOST', 'db.example.internal' );
define( 'EXT_DB_PORT', 3306 );
define( 'EXT_DB_NAME', 'catalog' );
define( 'EXT_DB_USER', 'catalog_reader' );
define( 'EXT_DB_PASSWORD', 'your-db-password' );
接続に使うデータベースユーザーには、必要な権限だけを与えます。表示用に読むだけなら SELECT のみを許可した専用ユーザーを作ります。WordPress本体と同じユーザー(DB_USER)を流用すると、プログラムの不具合が起きたときに本体のテーブルまで書き換えられる範囲が広がります。
サーバーによっては、環境変数や公開ディレクトリの外に置いた設定ファイルから getenv() で読み込む方法も使えます。どちらの場合も、値はWebから直接開けない場所に置き、リポジトリには含めません。
手順3:DB_HOSTの書式に対応して接続する
WordPress本体のデータベースにPDOで接続する場合、DB_HOST の書式に注意が必要です。WordPressの DB_HOST には localhost だけでなく、localhost:3307 のようなポート付きや、localhost:/var/run/mysqld/mysqld.sock のようなソケット指定も書けます。PDOのDSNではこれらを host、port、unix_socket に分けて指定する必要があります。
次の関数は、接続を1リクエストにつき1回だけ作り、使い回します。
<?php
/**
* Plugin Name: My PDO Connection
*/
function my_pdo(): ?PDO {
static $pdo = null;
if ( $pdo instanceof PDO ) {
return $pdo;
}
$host = defined( 'EXT_DB_HOST' ) ? EXT_DB_HOST : DB_HOST;
$port = defined( 'EXT_DB_PORT' ) ? (int) EXT_DB_PORT : null;
$name = defined( 'EXT_DB_NAME' ) ? EXT_DB_NAME : DB_NAME;
$user = defined( 'EXT_DB_USER' ) ? EXT_DB_USER : DB_USER;
$pass = defined( 'EXT_DB_PASSWORD' ) ? EXT_DB_PASSWORD : DB_PASSWORD;
$socket = null;
// "host:port" や "host:/path/to/socket" を分解する
if ( null === $port && false !== strpos( $host, ':' ) ) {
list( $host, $extra ) = explode( ':', $host, 2 );
if ( is_numeric( $extra ) ) {
$port = (int) $extra;
} elseif ( '' !== $extra ) {
$socket = $extra;
}
}
$dsn = $socket
? sprintf( 'mysql:unix_socket=%s;dbname=%s;charset=utf8mb4', $socket, $name )
: sprintf( 'mysql:host=%s;%sdbname=%s;charset=utf8mb4', $host, $port ? "port={$port};" : '', $name );
try {
$pdo = new PDO( $dsn, $user, $pass, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
) );
} catch ( PDOException $e ) {
error_log( 'my_pdo: connection failed (code ' . $e->getCode() . ')' );
return null;
}
return $pdo;
}
ポイントは3つです。DSNに charset=utf8mb4 を指定して、絵文字などを含む文字列の文字化けを防ぎます。PDO::ATTR_ERRMODE を例外モードにして、失敗を見落とさないようにします。PDO::ATTR_EMULATE_PREPARES を false にして、プレースホルダーの処理をデータベース側に任せます。
IPv6アドレスを DB_HOST に書いている環境では、この単純な分解ではうまく動きません。その場合は EXT_DB_HOST と EXT_DB_PORT を別々に定義してください。
手順4:値はプレースホルダーで渡す
SQLインジェクションを防ぐ基本は、利用者から受け取った値をSQL文字列に連結せず、プレースホルダーで渡すことです。
// 悪い例:入力値を文字列に埋め込んでいる
$sql = "SELECT * FROM products WHERE category = '" . $_GET['cat'] . "'";
$rows = my_pdo()->query( $sql )->fetchAll();
// 良い例:名前付きプレースホルダーで値を渡す
$cat = isset( $_GET['cat'] ) ? sanitize_key( wp_unslash( $_GET['cat'] ) ) : '';
$stmt = my_pdo()->prepare( 'SELECT id, name, price FROM products WHERE category = :cat LIMIT 50' );
$stmt->execute( array( ':cat' => $cat ) );
$rows = $stmt->fetchAll();
注意したいのは、プレースホルダーで渡せるのは「値」だけだという点です。テーブル名・列名・並び順(ASC/DESC)は渡せないため、許可する候補を配列で決めておき、その中から選びます。
$allowed_orders = array( 'name' => 'name', 'price' => 'price', 'new' => 'created_at' );
$key = isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'name';
$order_col = $allowed_orders[ $key ] ?? 'name';
$stmt = my_pdo()->prepare( "SELECT id, name FROM products ORDER BY {$order_col} ASC LIMIT 50" );
$stmt->execute();
LIKE で部分一致検索をする場合は、入力値の中の % と _ を事前にエスケープします。これを忘れると、利用者が % を入力しただけで全件に一致してしまいます。
$keyword = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
$like = '%' . addcslashes( $keyword, '\\%_' ) . '%';
$stmt = my_pdo()->prepare( 'SELECT id, name FROM products WHERE name LIKE :kw LIMIT 50' );
$stmt->execute( array( ':kw' => $like ) );
IN (...) に複数の値を渡したいときは、値の数だけ ? を並べて作ります。配列を一つの文字列にまとめてSQLに埋め込むことはしません。
手順5:書き込みは権限・nonce・トランザクションで守る
フォームや管理画面の操作からデータを書き込む場合は、データベース操作の前に、操作する人の権限と、リクエストが正規の画面から送られたものかを確認します。
add_action( 'admin_post_my_update_stock', function () {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( '権限がありません。', 'my-site' ), 403 );
}
check_admin_referer( 'my_update_stock' );
$id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$stock = isset( $_POST['stock'] ) ? absint( $_POST['stock'] ) : 0;
$pdo = my_pdo();
if ( ! $pdo || ! $id ) {
wp_die( esc_html__( '処理できませんでした。', 'my-site' ) );
}
try {
$pdo->beginTransaction();
$pdo->prepare( 'UPDATE products SET stock = :stock WHERE id = :id' )
->execute( array( ':stock' => $stock, ':id' => $id ) );
$pdo->prepare( 'INSERT INTO stock_log (product_id, stock, user_id) VALUES (:id, :stock, :uid)' )
->execute( array( ':id' => $id, ':stock' => $stock, ':uid' => get_current_user_id() ) );
$pdo->commit();
} catch ( PDOException $e ) {
if ( $pdo->inTransaction() ) {
$pdo->rollBack();
}
error_log( 'my_update_stock failed (code ' . $e->getCode() . ')' );
wp_die( esc_html__( '更新に失敗しました。', 'my-site' ) );
}
wp_safe_redirect( admin_url( 'admin.php?page=my-stock&updated=1' ) );
exit;
} );
フォーム側には wp_nonce_field( 'my_update_stock' ) を出力しておきます。トランザクションはInnoDBなどトランザクションに対応したストレージエンジンのテーブルでのみ意味を持ちます。
エラーメッセージと出力のエスケープ
PDOException のメッセージには、接続先のホスト名やSQL文の一部が含まれることがあります。画面に $e->getMessage() をそのまま表示せず、利用者には一般的なメッセージを、詳細はサーバーのログにだけ残します。本番環境では WP_DEBUG_DISPLAY を false にして、PHPのエラーが画面に出ないようにしておきます。ログにパスワードや個人情報を書き出さないことも大切です。
データベースから取り出した値をHTMLに出すときは、esc_html()、属性値なら esc_attr()、URLなら esc_url() でエスケープします。プレースホルダーはSQLインジェクションを防ぎますが、表示時のクロスサイトスクリプティングは防がないため、出力のエスケープは別に必要です。
まとめ
- WordPress内のデータ操作は
$wpdbが基本で、別のMySQL/MariaDBならnew wpdb()でも接続できる - PDOはWordPress外のデータや独立したテーブル向けに使い、本体のテーブルには書き込まない
- 接続情報は
wp-config.phpの定数などに置き、読み取り専用など権限を絞ったユーザーを使う DB_HOSTのポート・ソケット指定をDSN用に分解し、charset=utf8mb4と例外モードを指定する- 値はプレースホルダーで渡し、列名などは許可リストから選び、
LIKEは%と_をエスケープする - 書き込みは権限チェック・nonce・トランザクションで守り、エラー詳細は画面に出さずログに残す