📚
WordPress で do_action を用いる際の落とし穴:配列が自動的にアンパックされる謎の仕様
はじめに
WordPress のフックシステムは拡張性に優れた素晴らしい仕組みですが、時々予期せぬ動作に頭を抱えることがあります。今回は、do_action()
関数にまつわる意外な挙動について解説します。
遭遇した問題
最近、以下のようなコードを書いていました:
// $posts は WP_Post オブジェクトの配列
do_action( 'awesome_plugin_hook_name', $posts, $request );
一方、このアクションに対するコールバック関数は以下のように定義していました:
add_action( 'awesome_plugin_hook_name', 'my_callback_function', 10, 2 );
function my_callback_function( array $posts, WP_REST_Request $request ) {
// $posts が配列ではなく、単一の WP_Post オブジェクトになっている.
}
期待していたのは、$posts
が WP_Post オブジェクトの配列として渡されることでしたが、実際には単一の WP_Post オブジェクトが渡されていました。何が起きているのでしょうか?
原因究明
WordPress のコアコードを調査してみたところ、do_action()
関数内に以下のような処理があることがわかりました:
elseif ( is_array( $arg[0] ) && 1 === count( $arg[0] ) && isset( $arg[0][0] ) && is_object( $arg[0][0] ) ) {
// Backward compatibility for PHP4-style passing of `array( &$this )` as action `$arg`.
$arg[0] = $arg[0][0];
}
do_action 関数のコードを見る
function do_action( $hook_name, ...$arg ) {
global $wp_filter, $wp_actions, $wp_current_filter;
if ( ! isset( $wp_actions[ $hook_name ] ) ) {
$wp_actions[ $hook_name ] = 1;
} else {
++$wp_actions[ $hook_name ];
}
// Do 'all' actions first.
if ( isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $hook_name;
$all_args = func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
_wp_call_all_hook( $all_args );
}
if ( ! isset( $wp_filter[ $hook_name ] ) ) {
if ( isset( $wp_filter['all'] ) ) {
array_pop( $wp_current_filter );
}
return;
}
if ( ! isset( $wp_filter['all'] ) ) {
$wp_current_filter[] = $hook_name;
}
if ( empty( $arg ) ) {
$arg[] = '';
} elseif ( is_array( $arg[0] ) && 1 === count( $arg[0] ) && isset( $arg[0][0] ) && is_object( $arg[0][0] ) ) {
// Backward compatibility for PHP4-style passing of `array( &$this )` as action `$arg`.
$arg[0] = $arg[0][0];
}
$wp_filter[ $hook_name ]->do_action( $arg );
array_pop( $wp_current_filter );
}
これは簡単に言うと:
「do_action()
の第2引数として渡された配列が1つだけのオブジェクトを含む場合、WordPress はその配列を自動的にアンパックして、配列ではなく中身のオブジェクトを渡す」
つまり、もし $posts
が1つだけの WP_Post オブジェクトを含む配列だった場合、コールバック関数には配列ではなく、その単一の WP_Post オブジェクトが直接渡されるのです。
まとめ
do_action() に要素数1のオブジェクト配列を渡すと、PHP4 互換のレガシー処理により自動的にアンパックが行われ、配列ではなく単一のオブジェクトがコールバックに渡されます。意図しない挙動を防ぐには、受け取り側で配列チェックを行うか、do_action_ref_array() の利用で回避しましょう。
Discussion