
みなさんこんにちは、ガノー(Twitter:Ganohr)です。
WordPressには一時的な値を入出力するための『Transient API』(トランジェント・エーピーアイ)という機能があります。
今回はTransient APIについて軽く解説し、外部キャッシュ採用時にもTransient APIをDB(wp_options)へ同期して読み書きできる独自関数を定義して使用する例を公開します。
更新履歴
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
の結果を元に、条件分岐を行っていることを確認できるはずです。
wp_using_ext_object_cache
は外部キャッシュが有効になっているか否かを判定する関数であり、true
なら外部キャッシュが有効になっており、false
なら外部キャッシュを利用しておらず、直接DBへ値が入出力されます。
要はTransient APIは有効期限付きの値のみを保持するためのものであり、「どこかのタイミングで消されることを前提としている」訳であり、
値がDB上にあるか・キャッシュにあるかに依存する処理は実装上に誤りがある可能性が高いからです。
それでもなおこの関数が必要なパターン
WordPressの外部キャッシュには、制限があります。
それは
WordPressの外部キャッシュは、WordPressのインスタンスが有効な間のみの、そのインスタンス間でしか有効ではないという点です。
PHPを用いればファイルのアップローダやダウンローダを実装できますが、そうした機能を実装する際は、WordPressのインスタンスからは独立して実装される場合があります。
その際に、セキュリティ確保のための‘ノンス’(nonce
、認証に際して使用するランダムで比較的短いデータ)をWordPressとそのPHPファイルで共有する簡易的でセキュアな方法として、Transient APIを利用しているプラグインやテーマが多くあります。
WordPressのノンスは、12時間~24時間の間有効な期限付きの認証情報ですが、POST
やGET
に指定されたノンスが正しいか否かを判定するために、独立したPHP上でもノンスの検証が必要になります。
加えてTransient APIは有効期限を設定できるため、「ノンスが有効で、かつ、数分~1時間等ノンスよりも短い時間の間のアクセスを許可する」といった目的で利用されることがあります。
そのような場合は「外部キャッシュが有効な環境では、異なるWordPressインスタンスで実行されると、Transientが共有されておらず、誤動作を起こす」ことになります。
また、これはいうなればバグの一種ではあるものの、プラグインやテーマ開発元が外部キャッシュを利用していないためなかなか顕在化されません。
このような「バグのあるテーマやプラグインを改造せねばならないとき」、更には開発が滞っている場合などに「今回のAPIを利用するように改造すればそれだけでバグを改修できる」というわけです。
最後に
最近、キャッシュプラグインと相性の悪いテーマやプラグインを改修することが何度かありました。
WordPressのキャッシュプラグインは何年も前から利用されてきているとは言え、実際はその挙動についてあまり理解されずにテーマやプラグインが開発されているため、今回紹介したAPIを利用する場面が意外と多くあります。
今回のコードは、本来は利用するべきではありません。
新規開発するプラグインやテーマが外部キャッシュと相性が悪く、このコードを用いると正常に動作するといった場合は、外部キャッシュについて理解を深め、このコードを用いなくて済むようにコードを改修していくべきです。
されど、すでにあるレガシーコードをメンテナンスせねばならない場合には、この記事の内容は参考になるでしょう。
コメントを書く