【WordPress】Transientを問答無用で無効化する…ことはできなかった!

Transientをもりもり使用したテーマを構築した際、問題になるのは「変わらないんだけどキャッシュなの?バグなの?」というお客様からの質問。
お客様もわかりづらいし、私たちも調査などなどで時間を使ってしまいます。

管理者としてログインしているときにはフロントのTransientを効かせないようにすればいいんじゃないか?ということで、そんなことができるのかとコアファイルを覗いてみました。

[追記:2018/06/19]プラグインが公式プラグインディレクトリに掲載

ダッシュボードからボタン一つで全てのTransientがクリアできるプラグイン「Clear Transient From Dashboard」が公式ディレクトリに掲載されました。

get_transient()では返り値を一括制御できない

Transients APIについてはこちらを参照。
get_transient()で既にキャッシュされている値を取得、有効期限を確認して、期限が切れていたら値を作り直してset_transient()、切れていなかったら取得したキャッシュの内容を使用する…というのが基本の使い方。
Transientを効かせないというのは、「常にget_transient()がfalseを返せばいい」ということ。
それならと、うまいフィルターがないかどうか確認。

※wp-includes/option.php (WordPress 4.9.6)

/**
 * Get the value of a transient.
 *
 * If the transient does not exist, does not have a value, or has expired,
 * then the return value will be false.
 *
 * @since 2.8.0
 *
 * @param string $transient Transient name. Expected to not be SQL-escaped.
 * @return mixed Value of transient.
 */
function get_transient( $transient ) {

    /**  
     * Filters the value of an existing transient.
     *
     * The dynamic portion of the hook name, `$transient`, refers to the transient name.
     *
     * Passing a truthy value to the filter will effectively short-circuit retrieval
     * of the transient, returning the passed value instead.
     *
     * @since 2.8.0
     * @since 4.4.0 The `$transient` parameter was added
     *
     * @param mixed  $pre_transient The default value to return if the transient does not exist.
     *                              Any value other than false will short-circuit the retrieval
     *                              of the transient, and return the returned value.
     * @param string $transient     Transient name.
     */
    $pre = apply_filters( "pre_transient_{$transient}", false, $transient );
    if ( false !== $pre )
        return $pre;

    if ( wp_using_ext_object_cache() ) {
        $value = wp_cache_get( $transient, 'transient' );
    } else {
        $transient_option = '_transient_' . $transient;
        if ( ! wp_installing() ) {
            // If option is not in alloptions, it is not autoloaded and thus has a timeout
            $alloptions = wp_load_alloptions();
            if ( !isset( $alloptions[$transient_option] ) ) {
                $transient_timeout = '_transient_timeout_' . $transient;
                $timeout = get_option( $transient_timeout );
                if ( false !== $timeout && $timeout < time() ) {
                    delete_option( $transient_option  );
                    delete_option( $transient_timeout );
                    $value = false;
                }
            }
        }

        if ( ! isset( $value ) )
            $value = get_option( $transient_option );
    }

    /**
     * Filters an existing transient's value.
     *
     * The dynamic portion of the hook name, `$transient`, refers to the transient name.
     *
     * @since 2.8.0
     * @since 4.4.0 The `$transient` parameter was added
     *
     * @param mixed  $value     Value of transient.
     * @param string $transient Transient name.
     */
    return apply_filters( "transient_{$transient}", $value, $transient );
}

フィルターはいくつかあるけど、全てフィルター名に$transientを使っている…。
$transientにはTransientの名前が格納されているので、Transientの名前ごとにフックをかけていかなければならない…フィルターを使用して一括で返り値を変更するのは難しそう。

オブジェクトキャッシュも一枚噛んでる

また、wp_using_ext_object_cache()の分岐も注目。オブジェクトキャッシュも絡んできているよう。
wp_cache_get()の中を少し遡ってみたけど、フィルターはなさそうだったので、ここでfalseが返せないとオブジェクトキャッシュ使用時はアウトですね。

※wp-includes/load.php (WordPress 4.9.6)

/**
 * Toggle `$_wp_using_ext_object_cache` on and off without directly
 * touching global.
 *
 * @since 3.7.0
 *
 * @global bool $_wp_using_ext_object_cache
 *
 * @param bool $using Whether external object cache is being used.
 * @return bool The current 'using' setting.
 */
function wp_using_ext_object_cache( $using = null ) {
    global $_wp_using_ext_object_cache;
    $current_using = $_wp_using_ext_object_cache;
    if ( null !== $using )
        $_wp_using_ext_object_cache = $using;
    return $current_using;
}

うーん…これはどっかのタイミングで$_wp_using_ext_object_cacheをfalseに差し替えれば大丈夫そうかな?

get_option()でもどうにもできない

get_transient()内では返り値をfalseにはできなさそうだった…ということで、最後の砦、get_option()のフックを探してみる。
get_transient()内で、$transient_option = ‘_transient_’ . $transient; からの $value = get_option( $transient_option ); なので、get_option()に渡るときには、Transient名は全て ‘_transient_’ から始まるようになっている(DBを覗いたことがある人は、このコードを見なくても既に知っているかも)。
私の大嫌いな正規表現で「オプション名が’_transient_’ から始まったら false を返すよ!」ってしてあげればいいのでしょう。

…ということで、get_option()を覗いてみたのですが、あったフィルターはこちら!

※wp-includes/option.php (WordPress 4.9.6)

function get_option( $option, $default = false ) {
        ・・・省略・・・
    $pre = apply_filters( "pre_option_{$option}", false, $option, $default );
        ・・・省略・・・
            return apply_filters( "default_option_{$option}", $default, $option, $passed_default );
        ・・・省略・・・
    return apply_filters( "option_{$option}", maybe_unserialize( $value ), $option );
}

もれなく$option!
SQL文で値を取得して、それをreturnしているので、これ以上遡ることは不可。なんということでしょう。

残念な結論

結論:一括なんてするな!Transient名ごとにフックしろ!
add_filter()で正規表現使えたり、フック増えたりすると嬉しいのだけど…ここまで拘っているということは、一括フィルターは用意する気なんてないよってことなんでしょうね。
おとなしくTransient名ごとにフックを書いてあげるしかないみたいです。