【WordPress】外部キャッシュ有効でもDBと同期を取れるTransient API準拠の関数を定義する

【WordPress】外部キャッシュ有効でもDBと同期を取れるTransient API準拠の関数を定義する
【WordPress】外部キャッシュ有効でもDBと同期を取れるTransient API準拠の関数を定義する

 

 
読了目安:440

Are you want to translate this page to English? Please click this link to translate via ‘©Google Translate'!

ganohr.net favicon

みなさんこんにちは、ガノー(Twitter:Ganohr)です。

WordPressには一時的な値を入出力するための『Transient API』(トランジェント・エーピーアイ)という機能があります。

サムネイルTransients API - WordPress Codex 日本語版 ... 外部サイトへアクセスwpdocs.osdn.jp

 

今回はTransient APIについて軽く解説し、外部キャッシュ採用時にもTransient APIをDB(wp_options)へ同期して読み書きできる独自関数を定義して使用する例を公開します。

更新履歴
2022/12/19 コードをWordPressコーディング規約に準拠し修正
2022/12/12 公開

WordPressのTransient API準拠且つ外部キャッシュ有効でもDBと同期を取れる関数

このコードの著作権は放棄しておりません。アフィリエイト収益等を得られる状態でコードをコピーして公開することを禁止します。なお、自身の環境での活用は自由に行って構いません。
function ganohrs_build_transient_query( $table, $timeout, $key ) {
    global $wpdb;
    $option_name = "_transient_$timeout$key";
    return $wpdb->prepare(
        "
        SELECT
            option_id,
            option_value
        FROM
            $table
        WHERE
            option_name = '%s'
        ",
        $option_name
    );
}

function ganohrs_get_transient_real( $key ) {     if ( ! wp_using_ext_object_cache() ) {         return get_transient( $key );     }
    global $wpdb;     $table       = $wpdb->prefix . 'options';     $res_data    = $wpdb->get_row(         ganohrs_build_transient_query( $table, '', $key ),         ARRAY_A     );     $res_timeout = $wpdb->get_row(         ganohrs_build_transient_query( $table, 'timeout_', $key ),         ARRAY_A     );
    if ( $res_data === null ) {         if ( $res_timeout !== null ) {             $wpdb->delete(                 $table,                 array( 'option_id' => $res_timeout['option_id'] )             );         }         delete_transient( $key );         return false;     }     $now     = time();     $expires = ( $res_timeout !== null             && array_key_exists( 'option_value', $res_timeout )         ) ? $res_timeout['option_value'] : null;
    if ( $expires !== null         && $expires !== false         && is_numeric( $expires )         && $expires < $now     ) {         $wpdb->delete(             $table,             array( 'option_id' => $res_data['option_id'] )         );         $wpdb->delete(             $table,             array( 'option_id' => $res_timeout['option_id'] )         );         delete_transient( $key );         return false;     } elseif ( $expires === null || $expires === false ) {         $expires = 0;     }
    $obj = unserialize( $res_data['option_value'] );     set_transient( $key, $obj, $expires );
    if ( $obj === null ) {         return false;     }
    return $obj; }
function ganohrs_set_transient_real( $key, $value, $expires = 0 ) {     if ( ! wp_using_ext_object_cache() ) {         return set_transient( $key, $value, $expires );     }
    global $wpdb;     $table       = $wpdb->prefix . 'options';     $res_data    = $wpdb->get_row(         ganohrs_build_transient_query( $table, '', $key ),         ARRAY_A     );     $res_timeout = $wpdb->get_row(         ganohrs_build_transient_query( $table, 'timeout_', $key ),         ARRAY_A     );
    $now  = time();     $data = serialize( $value );
    if ( $res_data !== null && $res_data !== false ) {         $wpdb->update(             $table,             array(                 'option_name'  => "_transient_$key",                 'option_value' => $data,             ),             array( 'option_id' => $res_data['option_id'] )         );     } else {         $wpdb->insert(             $table,             array(                 'option_name'  => "_transient_$key",                 'option_value' => $data,             )         );     }
    if ( $res_timeout !== null && $res_timeout !== false ) {         if ( $expires === 0 ) {             $wpdb->delete(                 $table,                 array( 'option_id' => $res_timeout['option_id'] )             );         } else {             $wpdb->update(                 $table,                 array(                     'option_name'  => "_transient_timeout_$key",                     'option_value' => ( $now + $expires ),                 ),                 array( 'option_id' => $res_timeout['option_id'] )             );         }     } elseif ( $expires !== 0 ) {         $wpdb->insert(             $table,             array(                 'option_name'  => "_transient_timeout_$key",                 'option_value' => ( $now + $expires ),             )         );     }
    return set_transient( $key, $value, $expires ); }
function ganohrs_delete_transient_real( $key ) {     if ( ! wp_using_ext_object_cache() ) {         return delete_transient( $key );     }
    global $wpdb;     $table    = $wpdb->prefix . 'options';     $res_data = $wpdb->get_row(         ganohrs_build_transient_query( $table, '', $key ),         ARRAY_A     );
    if ( $res_data !== null ) {         $wpdb->delete(             $table,             array( 'option_id' => $res_data['option_id'] )         );     }     $res_timeout = $wpdb->get_row(         ganohrs_build_transient_query( $table, 'timeout_', $key ),         ARRAY_A     );     if ( $res_timeout !== null ) {         $wpdb->delete(             $table,             array( 'option_id' => $res_timeout['option_id'] )         );     }
    return delete_transient( $key ); }

API

API内容
ganohrs_get_transient_real($key)
 外部キャッシュが有効ならDBからキー($key)に指定された値を取得し、成功したら取得した値を元に外部キャッシュを更新し、返却する。その際、有効期限を超えていたらDB及び外部キャッシュから値を削除し、falseを返却する。
ganohrs_set_transient_real($key, $value, $expires = 0)
 外部キャッシュが有効ならDBにキー($key)で指定された値<(code>$value)を有効期限($expires)で指定された秒数間保管し、同時に外部キャッシュの値も更新する。その際、有効期限の指定に応じて、DB及び外部キャッシュの有効期限レコード(timeout)の指定も更新または削除を行い整合性を保つ。
ganohrs_delete_transient_real($key)
 外部キャッシュが有効ならDBからキー($key)で指定された値とその有効期限レコード(timeout)を削除する。

留意点:基本的にはこの関数を使わないで済むように設計することをお勧めします

WordPressはその構造から、純粋に単一のPHPで実装されたサイトよりも圧倒的に重くなります。WordPressは複数のPHPファイルが順々にロードされて、最終的に一つのページを出力するためです。

場合によっては1ページの内容を出力するためだけに何百ものPHPファイルが連携します。

 

そのためWordPressにおいては、外部キャッシュ機構による出力結果のキャッシュが行われていることがあります。

実際、Transient APIの実装コードを読んでいくと、いたるところでwp_using_ext_object_cacheの結果を元に、条件分岐を行っていることを確認できるはずです。

サムネイルWordPress.org ... 外部サイトへアクセスcore.trac.wordpress.org

 

wp_using_ext_object_cacheは外部キャッシュが有効になっているか否かを判定する関数であり、trueなら外部キャッシュが有効になっており、falseなら外部キャッシュを利用しておらず、直接DBへ値が入出力されます。

サムネイルwp_using_ext_object_cache() | Function | WordPress Developer ResourcesToggle $_wp_using_ext_object_cache on and off without directly touching global. ... 外部サイトへアクセスdeveloper.wordpress.org

 

要はTransient APIは有効期限付きの値のみを保持するためのものであり、「どこかのタイミングで消されることを前提としている」訳であり、

値がDB上にあるか・キャッシュにあるかに依存する処理は実装上に誤りがある可能性が高い

からです。

それでもなおこの関数が必要なパターン

WordPressの外部キャッシュには、制限があります。

それは

WordPressの外部キャッシュは、WordPressのインスタンスが有効な間のみの、そのインスタンス間でしか有効ではない

という点です。

 

PHPを用いればファイルのアップローダやダウンローダを実装できますが、そうした機能を実装する際は、WordPressのインスタンスからは独立して実装される場合があります。

その際に、セキュリティ確保のための‘ノンス’(nonce、認証に際して使用するランダムで比較的短いデータ)をWordPressとそのPHPファイルで共有する簡易的でセキュアな方法として、Transient APIを利用しているプラグインやテーマが多くあります。

サムネイルWordPress Nonce - WordPress Codex 日本語版 ... 外部サイトへアクセスwpdocs.osdn.jp

 

WordPressのノンスは、12時間~24時間の間有効な期限付きの認証情報ですが、POSTGETに指定されたノンスが正しいか否かを判定するために、独立したPHP上でもノンスの検証が必要になります。

加えてTransient APIは有効期限を設定できるため、「ノンスが有効で、かつ、数分~1時間等ノンスよりも短い時間の間のアクセスを許可する」といった目的で利用されることがあります。

 

そのような場合は「外部キャッシュが有効な環境では、異なるWordPressインスタンスで実行されると、Transientが共有されておらず、誤動作を起こす」ことになります。

また、これはいうなればバグの一種ではあるものの、プラグインやテーマ開発元が外部キャッシュを利用していないためなかなか顕在化されません。

 

このような「バグのあるテーマやプラグインを改造せねばならないとき」、更には開発が滞っている場合などに「今回のAPIを利用するように改造すればそれだけでバグを改修できる」というわけです。

最後に

最近、キャッシュプラグインと相性の悪いテーマやプラグインを改修することが何度かありました。

WordPressのキャッシュプラグインは何年も前から利用されてきているとは言え、実際はその挙動についてあまり理解されずにテーマやプラグインが開発されているため、今回紹介したAPIを利用する場面が意外と多くあります。

 

今回のコードは、本来は利用するべきではありません。

新規開発するプラグインやテーマが外部キャッシュと相性が悪く、このコードを用いると正常に動作するといった場合は、外部キャッシュについて理解を深め、このコードを用いなくて済むようにコードを改修していくべきです。

 

されど、すでにあるレガシーコードをメンテナンスせねばならない場合には、この記事の内容は参考になるでしょう。

WordPressの不具合対応/カスタマイズ¥15,000~

PC歴25年超、SE歴10年超、WordPress運営歴7年超、WordPressエンジニア歴5年超のスキルとノウハウを提供します

当サイト管理人の「ガノー」(Ganohr)は、日本最大手且つ東証一部上場企業が運営するクラウドソーシングサイト『Lancers』にて、認定ランサーとして活動しています。


※ 認定ランサーとはLancersにより様々な能力 ( 高い仕事遂行率・高い顧客満足度・多くの実績、など ) を評価したプロフェッショナルを認定する制度です。

 

PHPカテゴリの最新記事