
WordPressでプラグインやテーマをカスタマイズしていると、必ず出会うのが「フック」という仕組みです。「add_actionとadd_filterの違いがよく分からない」「優先度の数値を変えると何が変わるのかいまいち掴めない」と感じたことはありませんか。実はそのモヤモヤの正体は、WordPress 4.7でフックシステムの内部構造が大きく刷新されたことにあります。今回はその中核を担うWP_Hookクラスの挙動を、私の経験も交えながら丁寧に紐解いていきましょう。
WordPress 4.7以前と以後で何が変わったのか
私が初めてWordPressのフックを学んだ頃、コアのソースコードには$wp_filterという巨大なグローバル変数が配列として並んでいました。当時は、フックタグごとにコールバック関数のリストが多次元配列に格納されており、それをdo_actionやapply_filtersが順番に走査する、というシンプルな設計だったのです。
しかし2016年12月にリリースされたWordPress 4.7以降、その仕組みはオブジェクト指向へと刷新されました。配列で管理されていた情報がWP_Hookクラス(wp-includes/class-wp-hook.phpに定義)のインスタンスに置き換わり、$wp_filterグローバル変数は「タグ名とWP_Hookオブジェクトのマッピング」として機能するようになりました。
なぜこの変更が必要だったのかというと、コールバックの登録・解除・実行という一連のプロセスを、再現性のあるオブジェクトとして扱いたかったからです。配列のままだと、途中での優先度変更や反復処理が難しい。クラス化することで、メソッドとして振る舞いを切り出し、テストも行いやすくなります。
もうひとつの背景として、コールバックの登録情報そのものの構造化があります。従来の多次元配列では、関数の参照・引数の数・優先度の三つが一塊になっていましたが、これを「コールバックオブジェクト」として明示的に扱うことで、たとえばaccepted_argsだけを後から変更する、といった操作も安全に行えるようになったのです。表面的には同じように見えても、内部の「仕組み」を理解すると、トラブル時の切り分けがぐっと楽になりますよ。
$wp_filterとWP_Hookオブジェクトの管理プロセス
具体的な流れを追ってみましょう。あなたがadd_action('init', 'my_custom_init')のようなコードを記述したとき、WordPress内部では次のようなプロセスが走ります。
まず、WordPressは$wp_filterグローバルを参照し、'init'というタグに紐づくWP_Hookインスタンスが登録されているか確認します。まだ存在しなければ、新規にWP_Hookオブジェクトを生成して$wp_filter['init']に格納し、続けてWP_Hook::add_filter()メソッドを実行してあなたのコールバックを追加します。既に存在するタグであれば、既存インスタンスに対してadd_filter()が呼ばれ、配列の「最後」にコールバックが追記される、というわけです。
フックは「タグ(文字列)」と「WP_Hookオブジェクト」の二層構造で管理されている、というのがイメージの土台になります。
ここでひとつ意識しておきたいのは、$wp_filterが保持しているのはタグ名(文字列)をキーにした連想配列で、その各要素がWP_Hookオブジェクトである、という点です。つまり「タグを増やす」のではなく「タグに紐づくオブジェクトを増やす」という表現のほうが、構造としては正確ですね。
この設計のおかげで、フックが複数回呼ばれても情報が壊れず、WordPressの様々な箇所から同じタグに対してコールバックが登録されても秩序が保たれる、というわけです。
コールバックはオブジェクトとして保持される
WP_Hookクラスの中でも、特に理解しておきたいのがコールバックの保管形式です。タグに対して登録されたコールバックは、内部的には「コールバックオブジェクト」として配列化されています。具体的には、function(呼び出されるcallableへの参照)・accepted_args(そのコールバックが受け取る引数の数)・priority(実行優先度。ただし配列のキーとしても保持される)の三つがひとまとまりになった構造です。
これらが優先度ごとに$callbacksというプロパティに整理されて格納されているため、「タグ → 優先度 → コールバックオブジェクト」という三階層のナビゲーションで目的の関数を見つけ出せるようになっています。最初は入れ子構造に戸惑うかもしれませんが、この形こそが多数のコールバックを秩序立てて扱うための土台になっているのです。
フックの「中身」は、タグ → 優先度 → コールバックオブジェクト、という三層で考えると動きが追いやすくなります。
add_actionはadd_filterのラッパーである
ここで多くの方が驚く事実をお伝えします。実は、add_action()とadd_filter()は、コアの内部実装においては「同じもの」として扱われています。
WordPressのソースコードを読むと、add_action()の本体はadd_filter()をそのまま呼び出すだけの短いラッパーとして定義されていることが分かります。フックタグも、優先度も、受け取る引数の数も、まったく同じものがWP_Hook::add_filter()に集約されているのです。
| 観点 | add_action() | add_filter() |
|---|---|---|
| コア内部での実態 | add_filter()のラッパー | WP_Hook::add_filter()を直接呼び出す |
| 主な用途 | サイドエフェクト(戻り値なし) | データ加工(戻り値を次へ渡す) |
| デフォルト優先度 | 10 | 10 |
| デフォルト引数の数 | 1 | 1 |
両者の表面的な違いは、その呼び出し規約だけにとどまると言えるでしょう。デフォルトの値も全く同じで、優先度の既定値は10、受け取れる引数の既定値は1と決まっています。
つまり「アクションは値を返さないフィルターである」という設計思想が、そのまま関数の構造にも反映されているわけですね。この同一性を知ってしまうと、add_actionとadd_filterを別物として扱うのは「意味論上の都合」に過ぎない、という見方が自然に身についてきます。
do_actionは最終的にapply_filtersを呼ぶ設計
実行面に目を向けると、もう一段興味深い構造が見えてきます。do_action()を実行すると、内部ではWP_Hook::do_action()メソッドが動きます。そしてこのメソッドは、冒頭で$this->doing_action = true;というフラグを立てた上で、WP_Hook::apply_filters()を呼び出すという作りになっているのです。
つまり、アクションを実行しているときも、内部的には「フィルターの実行ロジック」を通っている、ということです。
アクションもフィルターも、最終的には同じ実行プロセス(apply_filters経由)を通っている。「何が違うのか」を意識すると、戻り値の扱いで踏み外さなくなります。
この設計になった理由を考えると、コールバックの優先度ソートや再帰呼び出しの制御といった複雑な処理を、一箇所に集約したかったのだと推測できます。コードの重複を避け、再現性のある実行プロセスとして管理する意図があったわけですね。
apply_filters()の内部では、$iterationsというプロパティが重要な役割を果たしています。これは「このフックが今、何回目の実行中か」をカウントするもので、同じフックの中で別のフックが再帰的に呼ばれたときに、無限ループや予期せぬ多重実行に陥らないための安全装置として機能します。具体的には、apply_filters()の冒頭で$iterationsを加算し、終了時に減算する、という仕組みで、過度な入れ子にならないよう制御しているのです。
優先度と実行順序のルール
コールバックが登録されると、それらは優先度(priority)の数値が小さいものから順に実行されます。優先度の値が同じであれば、追加された順番(登録順)に従う、というルールです。
実用上よく出てくるのは「自作プラグインの処理が、組み込みテーマの処理より先に走ってほしい」という場面ではないでしょうか。そんなときには、優先度を10ではなく1や5といった小さい値に設定します。逆に、組み込みの処理を上書きしたい場合は、20や30にして後ろに回すわけです。
ここで一つ、注意していただきたいのが「同じ優先度」と「登録順」の関係です。プラグインファイルが読み込まれる順番や、functions.php内での記述位置によって、同じ優先度でも実行順序が変わってしまうことに気づかない方は少なくありません。プロセスに再現性を持たせたいなら、優先度の数値を明示的に指定する癖をつけると安心です。
もうひとつ、優先度の扱い方で見落とされがちなのが、同じ優先度内での「登録順」です。たとえば自作プラグインが10で先に登録され、テーマのfunctions.phpが10で後に登録された、というケースでは、自作のコールバックが先に走ります。これを逆にしたい場合は、プラグイン側で意図的に1や5といった小さい優先度を取る、というのが定石ですね。
アクションとフィルターの意味的な使い分け
ここまでの話を踏まえると、アクションとフィルターは「技術的にほぼ同じ構造」であることはお分かりいただけたと思います。それでも私たちが両者を書き分けるのは、その「意味」が異なるからですよね。
apply_filters()は、渡されたデータを順に変形し、最終的な値を返す「データ変換」のために使います。フィルターのコールバックは必ずreturnし、その結果が次のコールバックへとバケツリレーされていくイメージです。
対してdo_action()は、特定のタイミング(init、wp_enqueue_scripts、save_postなど)で任意の処理を実行する「きっかけ(イベント)」のための仕組みです。戻り値は基本的に存在せず、画面への出力やデータベースへの書き込みといった副作用を起こすために使われます。
ここで初心者がよく踏み外すのが、「アクションフックでreturnした値が反映される」と誤解してしまうケースですね。do_action()は戻り値を利用しない設計のため、アクション内でreturnしても呼び出し元には渡されません。フィルタリング結果を得たいなら、迷わずapply_filters()を使う、という棲み分けを意識してみてください。
フィルターの戻り値設計で意外と多いのが、「複数のコールバックが連なることを意識していない」ケースです。最初のコールバックが文字列を返したのに、次のコールバックがそれを配列として扱おうとしてエラーになる、というパターンは現場で何度も見てきました。受け取った値の型を前提にするのではなく、自分のコールバックが呼ばれたときに実際に渡される型を都度確認する癖をつけると、原因の特定がずっと速くなります。
実践でつまずきやすいポイントと再現性
ここからは、私が現場でよく見かける「つまずき」をいくつか共有しますね。
一つ目は、フックの登録タイミングです。initより前の段階で特定のフックに登録しようとすると、$wp_filterにまだそのタグが存在しないため、新規WP_Hookの生成から始まります。これは想定通りの挙動ではあるのですが、規模が大きいプラグインだと「どの順番で読み込まれるか」が見えづらく、たまにデバッグが難しくなります。
二つ目は、コールバック関数名の衝突です。クラスを使わずに普通の関数名(my_custom_initなど)で登録していると、別のプラグインと同名だった場合に上書きされるリスクがあります。ここまで読んでくださった方であれば、WP_Hookの中で同じキーを持つコールバックが再登録されたときに何が起きるかを想像できるのではないでしょうか。
三つ目は、コールバック数の多発化です。一つのタグにあまりにも多くのコールバックが登録されると、優先度の調整が複雑になり、再現性が損なわれます。命名空間を使ったり、プラグインの設計を見直したりして、整理することを意識してみましょう。
四つ目は、再帰呼び出しによる想定外の挙動です。たとえばフィルターの中で同じフィルターを別の引数で呼んでしまうと、$iterationsが延々と増えていき、結果としてコールバックが期待した順序で走らないことがあります。デバッグの際には「いま自分がどのフックの何回目の実行中か」を意識できると、問題の切り分けがずっと楽になります。
五つ目は、コールバックの解除漏れです。プラグインをアンインストールしたのにremove_filter()やremove_action()が呼ばれず、不要な処理がそのまま残ってしまう、というのも現場でよく見るパターンです。WP_Hookクラスにはremove_all_filters()という強力なメソッドも用意されていますが、これを不用意に使うと他のプラグインにまで影響が及ぶため、対象を限定して解除する設計が望ましいですね。
さいごに:仕組みを理解すると設計が変わる
ここまで、WordPress 4.7以降のWP_Hookクラスを中心としたフックの内部構造を、順番に整理してきました。add_actionがadd_filterのラッパーであること、do_actionが内部的にapply_filtersの実行プロセスを通っていること、優先度と登録順のルール、そしてアクションとフィルターを使い分ける意味。
これらを頭に入れておくと、「とりあえず動いている」状態から一歩進んで、意図を持ってフックを設計できるようになります。次は、ご自分のプラグインやテーマのinitやwp_enqueue_scriptsに登録しているコールバックを一度棚卸ししてみてはいかがでしょうか。優先度が明示されているか、クラスで整理されているか、戻り値の扱いに一貫性があるか。その目でレビューしてみると、今まで気づかなかった改善ポイントが見つかるかもしれません。
フックの「プロセス」と「再現性」を味方につけて、ぜひWordPress開発を楽しんでくださいね。
Related reading: WordPressの自作プラグインとfunctions.phpの境界線:機能の肥大化を防ぐ設計ルール and WordPress・CMS設計をわかりやすく解説.