WordPressのsave_postフックで処理が2回実行される問題をトランジェントで解決した話 |岡山、広島、福山の人材支援、IT化支援の株式会社シーズ

WordPressのsave_postフックで処理が2回実行される問題をトランジェントで解決した話 - 株式会社シーズ|岡山、広島、福山の人材支援、IT化支援の株式会社シーズ

WordPressのsave_postフックで処理が2回実行される問題をトランジェントで解決した話

こんにちは、システム開発チームです。

WordPressで投稿保存時に特定の処理を実行する処理を構築していたところ、「処理が2回実行される」という現象に遭遇しました。

最初は「コードのどこかでミスったかな?」と思ったのですが、調べてみるとWordPressのsave_postフックの仕様が原因でした。今回はその原因とトランジェントAPIを使った解決方法を共有します。

こんな方へおすすめ

  • WordPressでフック使って開発されている方
  • 投稿保存時の重複実行でお困りの方
  • トランジェントAPIを利用されたことがない方

何が起きたのか

実装していたのは、投稿保存時にログを記録して外部APIにデータを送信する処理でした

function custom_post_save_handler( $post_ID, $post, $update ) {
    // ログを記録
    write_log( $post_ID, 'Post saved' );

    // 外部APIにデータを送信
    send_to_external_api( $post_ID, $post );
}
add_action( 'save_post', 'custom_post_save_handler', 10, 3 );

実装後、いざ固定ページを更新したら処理が2回走ってることが判明。。。

原因を調査:save_postフックの罠

色々調べた結果、save_postフックって1回の保存操作で複数回発火することがあるんですね。

なんで複数回発火するの?

主に以下の3つのパターンがあります

1. メタデータの更新で再帰的に呼ばれる

カスタムフィールドとかのメタデータを保存する時、WordPress内部でwp_update_post()が呼ばれて、それがまたsave_postをトリガーするみたいな状態に。

2. 投稿ステータスが変わる時

下書き→公開みたいなステータス変更の時も、複数回発火することがあります。

3. リビジョンの作成

リビジョン作成時にも発火しますが、これはwp_is_post_revision()でチェックできるので対処しやすい。

実際の動きを追ってみた

処理の流れを追跡したら、このような感じで2回実行されてました

ユーザーが「更新」ボタンをクリック
  ↓
save_post フック発火(1回目)
  → 処理実行(ログ記録、API送信)
  ↓
カスタムフィールドの保存処理が走る
  ↓
内部で wp_update_post() が実行される
  ↓
save_post フック発火(2回目)← ここで重複!
  → 処理再実行

どう解決するか考えた

いくつか対処法を検討してみました。

❌ 案1: グローバル変数で管理

global $processed_posts;
if ( in_array( $post_ID, $processed_posts ) ) {
    return;
}
$processed_posts[] = $post_ID;

ダメな理由: リクエスト単位でしか有効ではないので、Ajaxとかで動かすと意味なくなる可能性がある。

❌ 案2: remove_actionで自分を消す

function my_function( $post_ID ) {
    remove_action( 'save_post', 'my_function', 10 );
    // 処理
}
add_action( 'save_post', 'my_function', 10 );

ダメな理由: 他のプラグインとかからdo_action('save_post')が実行されたら対応できない。トリッキーすぎる。

❌ 案3: did_actionでカウント

if ( did_action( 'save_post' ) > 1 ) {
    return;
}

ダメな理由: 複数の投稿を連続で保存すると、2つ目以降が全部スキップされてしまう(致命的)。

✅ 採用: トランジェントで時間制限付きフラグ

最終的に、WordPressのトランジェントAPIを使う方法に落ち着きました。

理由としてはシンプルですが

  • 投稿ごとに独立して管理できる
  • 時間経過で自動的に消える
  • WordPress標準APIだから余計なライブラリ不要

トランジェントAPIって何?

トランジェントは、WordPress標準の有効期限付き一時データ保存機能です。要は「時限式のキャッシュ」みたいなものですね。

特徴

  • 指定した秒数で自動的に削除される
  • wp_optionsテーブルに保存される
  • RedisやMemcachedがあれば自動でそちらを使ってくれる
  • WordPress標準なので追加インストール不要

基本的な使い方

// データを保存(10秒間有効)
set_transient( 'my_key', 'my_value', 10 );

// データを取得(ないか期限切れならfalse)
$value = get_transient( 'my_key' );

// 削除(期限前に消したい時)
delete_transient( 'my_key' );

(シンプルで使いやすい)

実装してみた

最終的なコードがこちらです

function custom_post_save_handler( $post_ID, $post, $update ) {
    // リビジョンと下書きは除外
    if ( wp_is_post_revision($post_ID) || 
         in_array($post->post_status, ['auto-draft', 'draft'], true) ) {
        return;
    }

    // 投稿IDごとにユニークなキーを作る
    $transient_key = 'post_processed_' . $post_ID;

    // 既に処理済みならスキップ
    if ( get_transient( $transient_key ) ) {
        return;
    }

    // 処理済みフラグを10秒間セット
    set_transient( $transient_key, true, 10 );

    // 実際の処理
    write_log( $post_ID, 'Post saved' );
    send_to_external_api( $post_ID, $post );
}
add_action( 'save_post', 'custom_post_save_handler', 10, 3 );

コードの解説

1. ユニークなキーを作る

$transient_key = 'post_processed_' . $post_ID;

投稿IDを含めることで、複数の投稿を同時編集した場合でも問題ありません。

2. 処理済みかチェック

if ( get_transient( $transient_key ) ) {
    return;
}

トランジェントがあれば(= 10秒以内に処理済み)、即座に処理を中断します。

3. フラグを立てる

set_transient( $transient_key, true, 10 );

実際の処理を実行する前にフラグを立てるのがポイント。この後にsave_postが再発火しても、上のチェックで弾けます。

データベースへの影響は?

トランジェントはwp_optionsテーブルにこのような感じで保存されます

-- 値
option_name: _transient_post_processed_123
option_value: 1

-- 有効期限(UNIXタイムスタンプ)
option_name: _transient_timeout_post_processed_123
option_value: 1699350010

期限が来るとget_transient()の時に自動削除されますし、WordPressのクリーンアップ処理でも掃除されるので、ゴミが溜まる心配はありません。

トランジェントは他にも使える

重複防止だけではなくて、色々な場面で活躍します。

APIのキャッシュ

$cache_key = 'api_response_' . $user_id;
$response = get_transient( $cache_key );

if ( false === $response ) {
    $response = fetch_from_api( $user_id );
    set_transient( $cache_key, $response, HOUR_IN_SECONDS );
}

return $response;

レート制限

$limit_key = 'api_call_' . $user_id;

if ( get_transient( $limit_key ) ) {
    return new WP_Error( 'rate_limit', 'ちょっと待ってね' );
}

set_transient( $limit_key, true, MINUTE_IN_SECONDS );
// API処理

重い処理のキャッシュ

$cache_key = 'statistics_' . date('Y-m-d');
$stats = get_transient( $cache_key );

if ( false === $stats ) {
    $stats = calculate_heavy_statistics();
    set_transient( $cache_key, $stats, DAY_IN_SECONDS );
}

return $stats;

まとめ

WordPressのsave_postフックは便利ですが、複数回発火する仕様があるので注意が必要です。

学んだこと

  • save_postは1回の保存で複数回トリガーされることがある
  • トランジェントAPIで投稿ごとに独立した制御ができる
  • トランジェントはキャッシュやレート制限にも使える

同じ問題で困ってる人の参考になれば嬉しいです!

参考リンク