WordPress のデータベース操作の際に wp->prepare でテーブル名に変数を使おうとしてエラーになった件、%i というプレースホルダーで解決です。
01prepare を使う意味
テーマやプラグインを自作しないのであれば直接データベースを操作することはないですし、仮に自作する場合でも、SQL のテーブル名に変数を使わなくてはいけないケースも稀だとは思います。
ですのであまり一般的な事例ではありませんが、たまたま wp->prepare でテーブル名に変数を使ったためにエラーとなり、いろいろ調べたところ、WordPress Developer Resources の wpdb::prepare に %i というプレースホルダーがあることを知ったということです。
WordPress でデータベースを操作する場合は、SQLインジェクション対策のために $wpdb->prepare を使います。クエリに思わぬ SQL が入らぬようにエスケープするためです。たとえば、公開しているメインの記事のタイトル一覧を取得するには、
global $wpdb;
$query = "SELECT post_title FROM wp_posts WHERE post_status = 'publish' AND post_type = 'post'";
$result = $wpdb->get_results( $query );
print_r($result);
これでこと足りますが、クエリに変数を使う場合は、
global $wpdb;
$type = 'post';
$query = "SELECT post_title FROM wp_posts WHERE post_status = 'publish' AND post_type = %s";
$prepare = $wpdb->prepare( $query, $type);
print_r($prepare);
$result = $wpdb->get_results( $prepare );
print_r($result);
として、$wpdb->prepare を介して変数の値をエスケープします。
この例ではコード内で変数に値を入力していますので危険はないのですが、この $type の値をフォームデータや URL クエリから得るとしますと SQLインジェクションの危険が生じます。そこで上のコードのように $wpdb->prepare を使い、プレースホルダーで変数に値を与える方法を取ります。
SELECT post_title FROM wp_posts WHERE post_status = 'publish' AND post_type = 'post'
Array
(
[0] => stdClass Object
(
[post_title] => Hello world!
)
)
上のコードの結果です。WordPress を新規インストールしたままですので「Hello World!」ひとつだけタイトルが表示されています。1行目がデータベースに与えた SQL です。
02プレースホルダー %i
で、本題です。上のコードのテーブル名やフィールド(カラム)にも変数を使ったらどうなるんだろうということで次のコードを試してみました。
global $wpdb;
$table = 'wp_posts';
$column = 'post_title';
$status = 'publish';
$type = 'post';
$query = "SELECT %s FROM %s WHERE post_status = %s AND post_type = %s";
$prepare = $wpdb->prepare( $query, $column, $table, $status, $type);
print_r($prepare);
$results = $wpdb->get_results( $prepare );
print_r($results);
「SQL 構文にエラーがあります」のエラーになります。
原因はテーブル名とフィールド(カラム)のプレースホルダーに文字列用の %s を使っているからです。え? どうして? とかなり時間をつかいました。
SELECT 'post_title' FROM 'wp_posts' WHERE post_status = 'publish' AND post_type = 'post'
SQL 文を見てみますと、テーブル名とフィールドがシングルクオートで囲まれています。これが問題です。データベースの識別子は引用符なしか、囲むのであればバッククオートを使わないとエラーになります。
ということは、テーブルやフィールドには変数は使えないのかと思ったのですが、WordPress Developer Resources に答がありました。
プレースホルダー %d(整数), %f(浮動小数点数), %s(文字列)に続いて、
%i (identifier, e.g. table/field names)
%i があります。%i は、テーブルやフィールド(カラム)などに使うとあります。
03テーブル名を変数で与えるには %i を使う
global $wpdb;
$table = 'wp_posts';
$column = 'post_title';
$status = 'publish';
$type = 'post';
$query = "SELECT %i FROM %i WHERE post_status = %s AND post_type = %s";
$prepare = $wpdb->prepare( $query, $column, $table, $status, $type);
print_r($prepare);
$results = $wpdb->get_results( $prepare );
print_r($results);
うまくいきました!
SELECT `post_title` FROM `wp_posts` WHERE post_status = 'publish' AND post_type = 'post'
SQL 文を見てみますと確かにバッククオートで囲われています。
SQL でテーブル名をユーザーが指定するケースはあまりないかも知れませんが、フィールド(カラム)の場合は考えられますので、プレースホルダー %i を使うケースもありそうです。