WordPressのサイトをカスタマイズする時にお世話になるアクション/フィルターフック。

これに自作の関数をフックする(追加する)ことで様々な処理を追加することができる。

一方、プラグインなどはその機能としてフックを利用していることも多い。

そして場合によっては、プラグインなどがフックした関数を外したい場合もある。

しかし、フックするのは簡単でも、外すとなると一筋縄ではいかない場合もある。

今回は、その代表的な例である「クラス内の関数(メソッド)がフックされている場合」に、それをクラスの外側( functions.php など)から外す方法を調べてみた。

普通の関数の場合

まず参考までに、普通の関数を普通にフックした場合の外し方を以下に示す。

// フックされる関数の定義
function my_function() {
    /* 関数の処理 */
}
// このようにフックされた場合
add_filter( 'some_filter', 'my_function', 20 );
// このように外す
remove_filter( 'some_filter', 'my_function', 20 );

忘れてはならないのは、 add_filter() の第3引数に値が指定されている場合、 remove_filter() にも同じ値を指定しなければならないという点だ。

この値はフックする関数の優先順位で、初期値は「10」となっている。

以下の各例では優先順位の指定を省略する。

クラスによる変数のフックとは

クラスについての詳しい説明はここでは省略するが、WordPressのプラグインには関数ではなくクラスによって作られているものが少なくない。

そんなクラスの内部で関数として働くのがメソッドであり、クラスの内部でメソッドを以下のようにフックすることができる。

class MY_Class {
    function __construct() {
        // ここでフック
        add_action( 'some_action', array( $this, 'my_method' ) );
    }
    // フックされるメソッドの定義
    function my_method() {
        /* メソッドの処理 */
    }
}
new MY_Class();

これはプラグインがクラスを使って add_action() するシンプルな例だ。

メソッドをフックする時、 add_action() の第2引数には関数名の代わりに配列を使う。

この配列は、一つ目の要素がクラスのインスタンス、二つ目の要素はフックするメソッドの名前である。

さて、ではこのようにフックされたメソッドを、クラスの外側から外すにはどうすればよいのだろう。

クラス名とメソッド名

検索するとよく出てくるのが、以下の方法。

remove_action( 'some_action', array( 'MY_Class', 'my_method' ) );

配列にクラス名とメソッド名を指定するというものだ。

この方法はとてもシンプルだが、この方法が使えない場合もあるようだ。

If an action has been added from within a class, for example by a plugin, removing it will require accessing the class through a variable that holds the class instance. Unless the function is static in which case you could call the class and function directly.

出典:Function Reference/remove action « WordPress Codex

筆者による和訳:
もしアクションがプラグインなどによりクラスの内部で追加されたのなら、これを除去するにはそのクラスのインスタンスを含む変数を通じて、そのクラスにアクセスする必要があるだろう。その関数(訳注:メソッドのこと)がstatic(静的)であり、クラスと関数を直接コールすることができる、という場合でない限り。

すなわち、staticメソッドがフックされている場合ならこの方法が使える、ということ。

クラス情報を含むグローバル変数

プラグインがクラスのインスタンスを生成した時、それをグローバル変数に格納していれば、そのグローバル変数を利用できる。すなわち、

class MY_Class {
    /* 省略 */
}
// ここでインスタンス生成&格納
$my_class = new MY_Class();

このようなグローバル変数 $my_class が存在すれば、以下のように remove_action() できるということだ。

global $my_class;
remove_action( 'some_action', array( $my_class, 'my_method' ) );

しかし、そのようなグローバル変数を使用しないプラグインもあり、俺が今回遭遇したのはそういう場合だった。

「シングルトン」なクラス

このブログではプラグイン「Jetpack」の「Notifications」という機能を使っているのだが、こいつがHTML内に追加する多数のCSSやJavaScriptを除去しようとしていた。

管理ページ(ダッシュボード)以外ではヘッダーメニュー(ログイン中にWPサイトのページ上端に表示される黒いやつ)を表示しない設定にしているのに、これらは常に読み込まれてしまうのだ。

調べてみると、plugins ディレクトリの中の jetpack/modules/notes.php にある Jetpack_Notifications::action_init() に、その元凶のコードが存在することがわかった。

    function action_init() {
        /* 中略 */
        add_action( 'wp_head', array( &$this, 'styles_and_scripts'), 120 );
        add_action( 'admin_head', array( &$this, 'styles_and_scripts') );
    }
    function styles_and_scripts() {
        /* 省略 */
    }

admin_head は管理ページにおけるアクションだから、除去すべきは wp_head のほうになる。

しかし、このクラスは以下のようになっており、グローバル変数を持たず、 Jetpack_Notifications::action_init() はstaticメソッドでもない。

class Jetpack_Notifications {
    function __construct() {
        /* 中略 */
        add_action( 'init', array( &$this, 'action_init' ) );
    }
    public static function init() {
        static $instance = array();
        if ( !$instance ) {
            $instance[0] = new Jetpack_Notifications;
        }
        return $instance[0];
    }
    function action_init() {
        /* 省略 */
    }
    function styles_and_scripts() {
        /* 省略 */
    }
}
Jetpack_Notifications::init();

で、さらに調べていると以下の情報が見つかった。

リンク先にあるこの投稿に、答えがあった。

Or in case of a singleton class like Jetpack to remove the 'show_development_mode_notice' hook (for example) like this:

remove_action( 'jetpack_notices', array( Jetpack::init(), 'show_development_mode_notice' ) );

“singleton class”(シングルトンなクラス)とは、インスタンスを変数に格納せずとも、メソッドにより常に一意のインスタンスを得られる仕組みをもつクラスのこと、だと思う。

すなわち、上のコードでいうと Jetpack_Notifications::init() のようなstaticメソッドをもつクラスのことだ。

このメソッドは最初にコールした時は自身のインスタンスを生成し、その後はどこで何度コールしても、最初に生成したのと同じインスタンスを返してくれる。

つまり、グローバル変数を使わずに、外部からでもアクセス可能なインスタンスを保持できるというわけで、現状ではベストプラクティスであると目されている。

このメソッドを使えば、以下のようにフックを外すことができる。

remove_action( 'wp_head', array( Jetpack_Notifications::init(), 'styles_and_scripts' ), 120 );

最初の例のようなクラス

ところで、この記事で最初の例として挙げたクラスを思い出してほしい。

あのクラスはグローバル変数にインスタンスが格納されておらず、かといってstaticでもなく、シングルトンでもない。

何にも代入しておらず、単に new MY_Class(); しているだけだ。

この場合、どうすればいいのだろう。

いや、どうすることもできない。(反語)

すなわち、このように外部から参照できないような実装は、問題なく動作するが、決して真似するべきではないということだ。

……と思ったら、実はまだ方法があるらしい。

リンク先にあるこの投稿に示されている関数を使えば、力技だがどんなフックでもクラス名とメソッド名だけで外せるようだ。

なんでも、追加されたフィルター(アクションも)の一覧が、関数名・メソッド名などとともにグローバル変数 $wp_filter に格納されているようで、この中からクラス名やメソッド名を探し出してフックを外すことができるのだという。

なるほど、世の中には詳しい人がいるものだ……

まとめ

調べてみて、クラスのメソッドによって追加されたフックを解除するには以下の方法があることがわかった。

  • クラス名・メソッド名を使う - メソッドがstaticの場合に限る
  • クラスのインスタンスを代入した変数を使う - グローバル変数に代入済みの場合
  • シングルトンなクラスのインスタンスをメソッドで得る - そのように設計されているクラスの場合
  • グローバル変数 $wp_filter から探し出して解除する - 最終手段

実は俺、WordPressにはけっこう詳しくなったが、PHPのクラスについてはまだ勉強中だ。

将来的にプラグインを自作するようになったら、クラスにはシングルトンパターンを使うようにしたいと思う。

テーマ関数のクラス化を手がけるのもいいかもな。