📚

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