タグアーカイブ フック

WooCommerce 11.2のライフサイクルフック変更。保存と並び替えでSQL最大45%削減

WooCommerce 11.2のライフサイクルフック変更。保存と並び替えでSQL最大45%削減

WooCommerce 11.1と11.2で、商品保存と商品並び替えの内部処理が大きく変わった。商品保存時のSQLクエリ数は最大45%削減され、並び替えアルゴリズムは大規模カタログ向けに再設計された。

プラグインやカスタムコードで商品データを操作している場合、レガシーフックの移行が必要になる。本記事では変更の全体像、技術的な詳細、そして移行の具体的な手順を解説する。

WooCommerce 11.1と11.2の変更点を俯瞰する

WooCommerce 11.1と11.2の変更点を俯瞰する
今回の変更点の全体像
変更1 商品保存処理の最適化
不要なAPI呼び出しをスキップしてSQL数を最大45%削減
↓
変更2 商品並び替えアルゴリズムの刷新
レガシーフックを非推奨化して範囲ベースの再インデックスに移行
■ 保存処理(キャッシュ無効化の調整) ■ 並び替え処理(アルゴリズムとフック)

2つの変更はいずれも、後方互換性を優先しつつ、大規模な商品カタログでパフォーマンスを改善することを目的としている。

大規模カタログ向けのパフォーマンス改善

WooCommerceチームは、商品数が数千・数万を超えるストアで発生する管理画面の遅延やデータベース負荷を問題視していた。特に商品保存と並び替えは、カタログ全体に影響が及ぶ処理であり、これまでの実装ではカタログが大きくなるほど処理時間が線形に悪化する構造だった。

今回の変更は、商品データストアが不要な書き込みを行わないようにし、並び替えでは全カタログの再インデックスを避ける設計へと置き換えるものだ。これにより、大規模ストアの管理画面レスポンスが大幅に改善される見込みである。

後方互換性の優先とレガシーフックの扱い

重要なのは、非推奨になったフックを使い続けた場合、自動的に旧アルゴリズムへフォールバックする点だ。互換性は保たれるが、その分パフォーマンス改善の恩恵は受けられない。

プラグイン開発者にとっては、古いフックへの依存を解消し、新しいフックへ移行するかどうかの判断が必要になる。次のセクションから、各変更の詳細を見ていく。

商品保存処理の最適化でSQL数を45%削減

商品保存処理の最適化でSQL数を45%削減
従来の商品保存処理(Before)
商品を保存するたびに、値が変わっていなくても以下の処理を実行していた。
ターム関連APIの呼び出し(毎回)
メタデータ削除APIの呼び出し(毎回)
SQLクエリ数が多い
↓
最適化後の商品保存処理(After)
保存前の値と比較して、同じ値ならAPI呼び出しをスキップする。
ターム関連APIの呼び出し(変更時のみ)
メタデータ削除APIの呼び出し(変更時のみ)
SQLクエリ数が最大45%削減

商品保存時の動作が変わり、値に変更がない場合はAPI呼び出しをスキップする。この最適化により、保存1回あたりのSQLクエリ数が最大45%削減される。

no-op書き込みの削減がもたらす効果

今回の最適化は、wp_set_object_terms() と delete_post_meta() というWordPressの関数に着目したものだ。これらの関数は、データが変わっていない場合でも呼び出されるとキャッシュを無効化する動作をしていた。いわば「変更がないのに掃除をする」ような無駄が毎回発生していた。

WooCommerce 11.1以降では、保存前の値と保存後の値を比較し、同じ値であればこれらのAPIを呼び出さない。この小さな変更が、商品保存を頻繁に行う大規模ストアでは大きなDB負荷の削減につながる。

フック発火頻度の変化による副作用

ただし注意点がある。set_object_terms フックは、これまで商品保存のたびに発火していた。最適化後は、値が変わった場合のみ発火するようになる。このフックに依存して何らかの処理を実行しているカスタムコードがある場合、想定よりも発火回数が減る可能性がある。

具体的には、商品タイプが変わっていなくても毎回の保存で処理を行うことを前提としたコールバックは、移行が必要だ。次のセクションで確認手順を詳しく解説する。

商品並び替えアルゴリズムの刷新とフック移行

商品並び替えアルゴリズムの刷新とフック移行
商品並び替えフックの移行マップ
非推奨 woocommerce_after_single_product_ordering
カタログ再インデックス中に商品ごとに発火していた
↓
新フック woocommerce_product_ordering_process_reindexed_products
フルカタログ再インデックス後に発火
非推奨 woocommerce_after_product_ordering
並び替え完了後に一度だけ発火していた
↓
新フック woocommerce_product_ordering_process_moved_products
商品が再配置された後に発火
■ 非推奨フック(旧アルゴリズムへフォールバック) ■ 新フック(高速パス) ■ clean_post_cacheは変更なし

並び替えに関わるフックは2つが非推奨になり、2つの新しいフックが追加された。非推奨フックを使うと旧アルゴリズムにフォールバックする。

旧アルゴリズムから新アルゴリズムへ

従来の商品並び替えは、管理画面の「商品 → すべての商品 → 並び替え」で操作するたびに、カタログ全体を再インデックスしていた。商品数が1万件を超えると、この処理は数十秒かかることもあり、実用に耐えないケースがあった。

新しいアルゴリズムは、大規模カタログを前提に設計されている。再インデックスを高速化し、範囲ベースの並び替えを採用することで、変更された範囲だけを効率的に処理する。これにより、大規模ストアでも並び替え操作が快適になる。

非推奨フックと新フックの対応表

以下の表が、今回変更されたフックの全体像だ。非推奨になったフックを今も使っている場合、対応する新しいフックへの移行を検討する必要がある。

  • woocommerce_after_single_product_ordering は非推奨。カタログ再インデックス中に商品ごとに発火していた
  • woocommerce_after_product_ordering は非推奨。並び替え完了後に一度だけ発火していた
  • clean_post_cache は変更なし。レガシーと高速パスの両方で影響を受けた商品ごとに発火する
  • woocommerce_product_ordering_process_reindexed_products は新設。フルカタログ再インデックス後に発火する
  • woocommerce_product_ordering_process_moved_products は新設。商品が再配置された後に発火する

元の動作を再現するには、clean_post_cache、wp_ajax_woocommerce_product_ordering、woocommerce_product_ordering_process_reindexed_products、woocommerce_product_ordering_process_moved_products をプリミティブとして組み合わせる方法が公式に案内されている。

既存サイトが受ける影響と確認手順

既存サイトが受ける影響と確認手順
AIツールを使った確認手順
STEP 1 アクティブなプラグインとテーマの functions.php を対象に検索
↓
STEP 2 add_action(‘set_object_terms’, …) のコールバックを探す
↓
STEP 3 商品タイプが変わっていなくても毎回発火する前提かチェック
↓
STEP 4 該当する場合は woocommerce_update_product や save_post_product への移行を検討

公式ブログでは、AIツールを使ってカスタムコードを確認する方法が提示されている。上の手順で、フックへの依存を洗い出せる。

カスタムコードの確認方法

まず、アクティブなプラグイン、MUプラグイン、テーマの functions.php を対象に、add_action('set_object_terms', ...) というコールバックを検索する。該当があれば、そのコールバックが「商品タイプが変わっていなくても毎回発火すること」を前提にしていないかを確認する。

もし前提にしていた場合、そのコールバックは woocommerce_update_product や save_post_product など、より適切なフックへ移動する必要がある。

移行が必要なケースと不要なケース

移行が必要なのは、set_object_terms フックが毎回発火することを前提にしたコードが存在する場合だ。逆に、商品タイプの変更やカテゴリの変更など、実際に値が変わる場合のみ処理を行いたいというコードは、そのままでも問題ない。

並び替え関連では、woocommerce_after_single_product_ordering と woocommerce_after_product_ordering を使っているコードは、新しいフックへの移行を検討する。非推奨フックを使い続けても動作はするが、旧アルゴリズムにフォールバックするため、パフォーマンス改善の効果を得られない。

この記事のポイント

  • 商品保存処理は、値が変わらない場合はAPI呼び出しをスキップしてSQL数を最大45%削減する
  • 商品並び替えは、全カタログ再インデックスから範囲ベースの新アルゴリズムへ刷新された
  • set_object_terms フックの発火頻度が変わるため、毎回発火を前提としたコードは移行が必要
  • 並び替えの旧フック2つは非推奨になり、新フック2つが追加された
  • 非推奨フックを使い続けると旧アルゴリズムにフォールバックし、高速化の恩恵を受けられない
WordPressのdo_action()を見直す イベントをオブジェクト化し、フックをクラス名にする最新の開発パターン

WordPressのdo_action()を見直す イベントをオブジェクト化し、フックをクラス名にする最新の開発パターン

WordPressの開発でdo_action()を一度も使ったことがない人はまずいないだろう。カスタムフックを定義して他のプラグインやテーマから振る舞いを拡張する手法は、バージョン1.2で導入されて以来変わらず愛用されている。

しかし長年の使い込みの中で、いくつかの「小さな摩擦」が無視されてきたのも事実だ。引数の順序を覚えきれない、フック名が大域空間で衝突する、戻り値の返し忘れでフィルターが壊れる、といった問題は大規模なコードベースになるほど開発者の時間を食う。

この記事では、do_action()に1つのオブジェクトを渡し、そのクラス名をフック名として使うことで、これらの問題を一掃するパターンを解説する。独自のライブラリもフレームワークも不要で、PHPの標準機能だけで実装できる。2026年8月にDeveloper WordPress Newsで公開された洞察をもとに、実装例と仕組みを詳しく見ていこう。

do_action()で起きていた4つの摩擦

do_action()で起きていた4つの摩擦

従来のdo_action()は「フック名」と「任意の数の引数」を受け取るシンプルなAPIだ。会員登録をトリガーとするプラグインであれば、次のようにフックを発火させるのが一般的な書き方になる。

do_action( 'myplugin_member_registered', $userId, $plan );

これを受け取る側のコードは以下のようになる。

add_action( 'myplugin_member_registered', static function ( $userId, $plan ) {
    // ... 何らかの処理
}, 10, 2 );

動作自体に問題はない。だが、よく見ると運用上のストレスがいくつも潜んでいる。Developer WordPress Newsの記事が指摘する摩擦点は大きく次の4つだ。

  • 引数が位置に依存する $userIdと$planの順序を覚えておかなければならず、間違えた場合のエラーは追跡しにくい。
  • フック名がグローバルな名前空間 'myplugin_member_registered'という文字列が他のプラグインと衝突しないよう、prefixをつけて管理する必要がある。
  • 型情報が一切ない $planは文字列かもしれないしIDかもしれない。コードエディタの補完も静的解析も効かず、ドキュメントを読まなければ正体がわからない。
  • フィルターの戻り値忘れ apply_filters()を使うと、どのリスナーも必ず値を返さなければならない。うっかりreturnを書き忘れると、後続のフィルターがすべて破綻する。

これらはフックの仕組みそのものの問題ではなく、ペイロード(運ぶデータ)の渡し方に起因する。そして、このペイロード設計を見直すだけで、すべてがきれいに解決する。

従来のフック呼び出し(Before)
do_action( ‘myplugin_member_registered’, $userId, $plan );
※フック名は文字列でグローバルスコープ、引数は位置で決まるため順序を覚えておかなければならない
↓
オブジェクトを使った新しいパターン(After)
do_action( MemberRegistered::class, $event );
※完全修飾クラス名がフック名になり、オブジェクト1つですべての情報を運ぶ。型が自動的に決まる

上の比較でわかるとおり、do_action()に渡す情報をオブジェクトにまとめるだけで多くの摩擦が消える。フック名の衝突リスクがなくなり、コードエディタの補完も効く。

イベントオブジェクトとクラス名フック 1行で変わる設計

イベントオブジェクトとクラス名フック 1行で変わる設計

解決策の核心は驚くほど単純だ。Developer WordPress Newsの記事の著者が試みたのは、「引数をバラバラに渡すのではなく、起こった出来事を表すオブジェクトを1つだけ渡す」というやり方である。

具体的には、次のように書く。

do_action( $event::class, $event );

$event::classが何が起きたかを示すフック名になり、$eventがその詳細を運ぶ。フック名にはそのクラスの完全修飾名が使われるため、名前空間が自動的に適用され、他のプラグインと衝突することはまずない。

フック名を::classで決めるか、あるいは固定の文字列にするかは選択できる。クラス名を使えば、IDEでクラスをリネームするとフックも追従する。文字列(例:'myplugin/member-registered')を指定すれば、後からクラスを移動させてもフック名は変わらない。移行の危険を減らしたいなら固定文字列のほうが安全だが、どちらにせよペイロードは整ったオブジェクトになる。

また、このテクニックはapply_filters()を使わなくてもフィルター的な振る舞いを実現できる。オブジェクトのプロパティを書き換え可能にしておけば、リスナーが自由に値を変更し、ディスパッチャ側が後から読み取れるからだ。戻り値の管理に頭を悩ませる必要がなくなる。

具体例 会員登録のDispatcherとListener

具体例 会員登録のDispatcherとListener

ここでは会員登録を題材に、イベントオブジェクトを使ったフックの流れを具体的に見ていこう。名前空間MyPlugin\Membersの下に、登録イベントを表すクラスと、それを発行するクラス、そして受け取る側のコードを用意する。

イベントクラスの定義

namespace MyPlugin\Members;

final class MemberRegistered
{
    public function __construct(
        public readonly int    $userId,
        public readonly string $plan,
        public          bool   $sendWelcomeEmail = true,
    ) {}
}

$userIdと$planはreadonlyとして宣言され、リスナーは読み取り専用で使用する。一方、$sendWelcomeEmailは書き換え可能にしておき、ウェルカムメールを送るかどうかの判断をリスナーに委ねる。

Dispatcher(発行側)

namespace MyPlugin\Members;

final class MemberRegistrar
{
    public function register( int $userId, string $plan ): void
    {
        // アカウント作成処理...

        $event = new MemberRegistered( userId: $userId, plan: $plan );

        do_action( $event::class, $event );

        if ( $event->sendWelcomeEmail ) {
            // ウェルカムメールをキューに追加
        }
    }
}

ポイントは、do_action()の後で$event->sendWelcomeEmailを評価しているところだ。この値は後続のリスナーが変更したかもしれない。オブジェクトは参照で渡されるため、ディスパッチャはリスナーによる変更をそのまま拾える。ここにapply_filters()は必要ない。

ObserverとMutatorの2パターン

受け側はおおまかに2種類に分かれる。1つはイベントを観測するだけのObserverで、値を読み取るが変更はしない。もう1つはプロパティを書き換えるMutatorだ。

// Observer ログ出力のみ
add_action( MemberRegistered::class, static function ( MemberRegistered $event ): void {
    error_log( sprintf(
        'Member #%d registered on the %s plan.',
        $event->userId,
        $event->plan
    ) );
} );

// Mutator 無料プランの場合はウェルカムメールを無効化
add_action( MemberRegistered::class, function ( MemberRegistered $event ): void {
    if ( 'free' === $event->plan ) {
        $event->sendWelcomeEmail = false;
    }
} );

Mutatorのコールバックを見ると、型ヒントによって$eventのプロパティが自動補完されることがわかる。$userIdや$planの順序を気にする必要はなく、add_action()の第4引数で引数の数を指定する手間もない。これがオブジェクト化の地味ながら大きな恩恵だ。

Observerの動作
イベント → ログ記録
イベントオブジェクトを読むだけで、値を変更しない。
↓
Mutatorの動作
イベント → プロパティ変更 → Dispatcherが後で読み取り
$sendWelcomeEmailをfalseにすることで、メール送信の挙動を変更する。
■ イベントオブジェクト  ■ Observer  ■ Mutator  ■ Dispatcherの後工程

このように、1つのオブジェクトを受け渡すだけで、従来のアクションとフィルターの両方の役割を統一的に扱える。コードの見通しは飛躍的に良くなるはずだ。

オブジェクト化がもたらすメリットのまとめ

オブジェクト化がもたらすメリットのまとめ

これまでの例から、イベントオブジェクトパターンを採用することで得られる具体的な利点を整理する。

  • 型安全なリスナー すべてのコールバックがイベントクラスで型を宣言するため、エディタの補完と静的解析がフルに働く。
  • 名前衝突からの解放 フック名は完全修飾クラス名(または明示的な文字列)で管理されるため、prefixを手動で付ける必要がなくなる。
  • フィルターのような書き換えをアクションで実現 オブジェクトのプロパティをミュータブルにしておけば、apply_filters()を使わずとも値の変更が可能。戻り値の返し忘れによるバグも消える。
  • 自己文書化 各イベントクラスが独立したファイルになるため、どの拡張ポイントが存在するかがディレクトリを見るだけで把握できる。

これらの改善は、特別なライブラリを導入しなくても、今日から自社のプラグインに適用できる。既存のdo_action()やadd_action()をそのまま使いつつ、ペイロードをオブジェクトに切り替えるだけだ。

WordPressのルーツとPSR-14の関係

WordPressのルーツとPSR-14の関係

このパターンを「新しいアイデア」と感じるかもしれないが、実はWordPressが2004年のバージョン1.2で搭載したプラグインAPIの時点で、根幹の設計は既に存在していた。

PHPの世界でPSR-14(Event Dispatcher)として標準化された「イベント、リスナー、ディスパッチャ」の概念は、do_action()とadd_action()の組み合わせでほぼ表現できる。つまり、WordPressはとっくにイベント駆動の基盤を持っていたことになる。

PSR-14が定める厳密な契約(伝播制御、リスナープロバイダー、ディスパッチャの交換など)はWordPressには組み込まれていない。だが、「イベントをオブジェクトで表現し、そのクラス名でフックする」という発想は、PSR-14のイベントモデルと驚くほど調和する。結果として、WordPressのネイティブな関数の上に、よりモダンで安全なイベント設計を載せられるわけだ。

Developer WordPress Newsの記事の著者も、PSR-14を完全実装する試みの中で、大半の価値がこの小さな習慣に詰まっていることに気づいたと述べている。段階的な移行が可能で、いきなり大掛かりなフレームワークに乗り換える必要はない。

このパターンの限界と発展の方向性

このパターンの限界と発展の方向性

ここまでのテクニックで、自作プラグインのカスタムフックの多くは十分に改善できる。ただし、次のような高度な要件に直面したときは、より本格的なイベントシステムの導入を検討してもいい。

  • 伝播の停止 あるリスナーが「以降のリスナーは実行するな」と指示する仕組み。これはremove_action()と優先度では再現しきれない場面がある。
  • 複雑なリスナー管理 数十個のリスナーを手作業で登録するのではなく、1つのサブスクライバークラスにまとめて登録したいケース。
  • テストのための差し替え ディスパッチャ自体を丸ごと入れ替えて、イベントの発生をテストダブルで差し替えたい状況。

これらの必要性を感じ始めた時が、PSR-14準拠のディスパッチャを導入する潮時といえる。しかし、そこに至るまでは、「do_action()にオブジェクトを渡す」習慣だけで、保守性も開発体験も大きく前進させられる。

今日からすぐに実践できる小さな設計変更が、コードベースの品質を長期にわたって支える土台になる。次のカスタムフックを書くときに、このパターンをぜひ試してみてほしい。

この記事のポイント

  • do_action()にバラバラの引数ではなく1つのイベントオブジェクトを渡すと、引数の順序問題や型の不明瞭さが解消される。
  • フック名にクラスの完全修飾名を使うことで、名前空間が自然に確保され、衝突リスクが劇的に下がる。
  • オブジェクトの書き換え可能プロパティを利用すれば、apply_filters()を使わずにフィルター的な挙動を安全に実装できる。
  • オブザーバーとミューテーターの2種類のリスナーを型安全に記述でき、コードエディタの補完がフルに働く。
  • PSR-14のような本格的なイベントシステムを導入しなくても、小さな習慣を変えるだけで開発体験が大幅に改善する。