結論
- LIKE 検索の
%は、SQL文に直接書かず、値の側に含めて%sで渡します。ユーザー入力の%や_は$wpdb->esc_like()で先にエスケープします。 - IN 句は、要素数ぶんの
%d(文字列なら%s)をimplode()で組み立て、値は配列でprepare()に渡します。 - テーブル名やカラム名は、WordPress 6.2.0 から追加された
%iで渡せます。それ以前の環境では使えません。 sanitize_text_field()は SQLインジェクション対策ではありません。SQLに値を入れるときは必ずプレースホルダを使います($wpdb->prepare() の使い方|説明・引数・注意点)。
原因・仕組み
$wpdb->prepare() は、%s(文字列)、%d(整数)、%f(小数)、%i(識別子)の4つのプレースホルダに対応しています。値はエスケープされ、文字列は引用符で囲まれます。
問題は、LIKE の % とプレースホルダの % が見た目で同じことです。公式ドキュメントも、LIKE のワイルドカードは「完全なLIKE文字列を持つ置換引数」で渡すよう求めています。そのため、次のように書きます。
| やりたいこと | 書き方 |
|---|---|
| 前後一致(部分一致) | '%' . $wpdb->esc_like( $kw ) . '%' を %s に渡す |
| 前方一致 | $wpdb->esc_like( $kw ) . '%' |
| 数値のIN | IN (%d,%d,%d) を配列から生成 |
| 文字列のIN | IN (%s,%s) を配列から生成 |
手順(サンプルコード)
global $wpdb;
// 1) LIKE: ユーザー入力の % と _ を esc_like で無効化してから % を足す
$keyword = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : '';
$like = '%' . $wpdb->esc_like( $keyword ) . '%';
$ids = $wpdb->get_col(
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_status = %s AND post_title LIKE %s", 'publish', $like )
);
// 2) IN: 個数が可変のとき
$post_ids = array_map( 'absint', (array) $post_ids );
$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
$sql = $wpdb->prepare( "SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ($placeholders)", $post_ids );
$rows = $wpdb->get_results( $sql );
2) では、$post_ids が空配列だと IN () になり、SQLエラーになります。空のときは先に return します。
動作確認(検証環境と結果)
WordPress 7.1.2(ja)、PHP 8.2.12、MariaDB 10.4.32。php run.php wpdb-prepare-like-in.php で確認しました。
SELECT ID FROM wp_posts WHERE ID IN (3,5,7)
SELECT ID FROM wp_posts WHERE post_type IN ('post','pa'ge')
SELECT * FROM `wp_posts` WHERE ID = 1 // %i と %d
count Hello: 1 // 実際にクエリを実行した件数
- 配列
array(3, 5, '7; DROP TABLE x')を%dに渡すと、3つ目は7に変換されました(文字列の後半は捨てられる)。 - 文字列の
pa'geはpa'geとエスケープされました。 %を含む LIKE のprepare()結果をechoすると、%の部分が長い16進の文字列(プレースホルダ用のエスケープ値)に置き換わって表示されました。これはprepare()が内部で%を保護している表示で、クエリの実行時(get_var()など)には元に戻ります。デバッグ時にechoした値を直接DBに貼らないようにしてください。- 悪い例として、
LIKE '%foo%' AND ID = %dと直接書く方法を試したところ、戻り値は空文字でした('%foo%'の%fがプレースホルダと解釈され、引数の個数が合わずエラー扱いになった)。このように書くのは避けます。
$wpdb->prepare で LIKE と IN 句を安全に書く方法の注意点
esc_like()は LIKE 用の%_のエスケープだけを行います。SQLインジェクション対策はprepare()が担当し、両方必要です。順序はesc_like()→prepare()です。prepare()に値を渡さずに%が無い文字列を渡すと、公式では警告が出ると書かれています。値がないSQLにprepare()は不要です。- 番号付きプレースホルダ(
%1$s)は、公式ドキュメントにあるとおり引用符が自動で付きません(検証環境でもSELECT %1$sにa'bを渡すとa'bと引用符なしで展開され、引数の個数不一致の Notice も出ました)。基本は%s%dを使います。引数は個別か配列のどちらかで統一します(混ぜない)。 %dに渡す値は、事前にabsint()などで整数にしておくと意図が明確です。
$wpdb->prepare で LIKE と IN 句を安全に書く方法でよくあるミス
"... LIKE '%{$kw}%'"と変数を直接埋め込む(悪い例。インジェクションの原因)。prepare()の結果をさらに文字列連結して使う。- IN 句で
%sに数値の配列を渡し、数値が'1'のように引用される。 - テーブル名を
%sで渡し、'wp_posts'と引用符付きになる。
$wpdb->prepare で LIKE と IN 句を安全に書く方法のチェックリスト
- 値はすべてプレースホルダで渡しているか。
- LIKE の入力値に
esc_like()を使っているか。 - IN 句の配列が空のケースを先に処理しているか。
- 識別子は、固定値か、
%i(6.2以降)で渡しているか。
$wpdb->prepare で LIKE と IN 句を安全に書く方法のFAQ(よくある質問)
Q. $wpdb->esc_sql() で足りませんか?
A. 古い書き方ですが、新しいコードでは prepare() を使います(esc_sql() の使い方|説明・引数・注意点)。
Q. LIKE で大文字小文字を区別しますか?
A. DBの照合順序(collation)によります。WordPress標準では、通常は区別しません。
Q. 検索なら WP_Query の s で十分では?
A. 投稿の検索なら WP_Query を優先してください。独自テーブルや特殊な条件のときにだけ $wpdb を使います。
筆者の見解($wpdb->prepare で LIKE と IN 句を安全に書く方法)
SQLは「値はすべて prepare() に任せ、文字列連結は構造(テーブル名と個数ぶんのプレースホルダ)だけ」と決めておくと、レビューが楽になると考えます。LIKE と IN は、どちらも機械的に書けるのに事故が多い箇所なので、小さなヘルパー関数にまとめて使い回すのがよいと思います。
$wpdb->prepare で LIKE と IN 句を安全に書く方法の関連項目
- $wpdb->prepare() の使い方|説明・引数・注意点
- $wpdb->esc_like() の使い方|説明・引数・注意点
- $wpdb->get_results() の使い方|説明・引数・注意点
- $wpdb->get_col() の使い方|説明・引数・注意点
- esc_sql() の使い方|説明・引数・注意点
