こんにちは、WordPressのパフォーマンスを向上させるための方法を日々研究しています。今日はその中でも、永続オブジェクトキャッシュ(APCu)の使用方法について紹介します。

永続オブジェクトキャッシュとは?
永続オブジェクトキャッシュは、PHPの拡張モジュールの一つで、データベースのクエリ結果や計算結果などのデータを高速にキャッシュすることができます。これにより、同じデータへのアクセスが高速化され、サイトのパフォーマンスが向上します。

WordPressでのAPCuの使用方法

APCuのインストール
最初に、サーバーにAPCuをインストールする必要があります。多くのホスティング環境では、すでにインストールされている場合がありますが、自分で管理しているサーバーの場合は手動でインストールする必要があります。

サイトで永続オブジェクトキャッシュを有効化できるかどうかは、ダッシュボードから、サイトヘルスステータスを確認することでわかります。

お使いのホスティングサービスで、オブジェクトキャッシュサービスをサポートしている場合は、

お使いのホスティングサービスでは、次のオブジェクトキャッシュサービスをサポートしているようです: APCu, Redis。

のように、表示されます。

WordPressのオブジェクトキャッシュをAPCuで使用する
WordPressのオブジェクトキャッシュをAPCuで使用するには、'object-cache.php'を wp-content内に配置し、キャッシュ関数 wp_cache_*() をAPCu版に置き換えることで使用できます。


<?php //(object-cache.php)

/**
 * WPのオブジェクトキャッシュをAPCuで使用する
 * (wp-content内のobject-cache.phpとして配置する)
 */

//直接のアクセスを防ぐ
if (!defined('ABSPATH')) {
  exit;
}

//APCuが利用できない場合は終了
if (!function_exists('apcu_fetch')) {
  return;
}

if (function_exists('wp_cache_add')) {
  //Regular die, not wp_die(), because it gets sandboxed and shown in a small iframe
  die('<strong>ERROR:</strong> This is <em>not</em> a plugin, and it should not be activated as one.<br /><br />Instead, <code>' . str_replace($_SERVER['DOCUMENT_ROOT'], '', __FILE__) . '</code> must be moved to <code>' . str_replace($_SERVER['DOCUMENT_ROOT'], '', trailingslashit(WP_CONTENT_DIR)) . 'object-cache.php</code>');
} else {
  //キャッシュ関数 wp_cache_*() をAPCu版に置き換える
  //オーバーライド関数をここに
  function wp_cache_add($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->add($key, $data, $group, (int) $expire);
  }
  function wp_cache_close() {
    return true;
  }
  function wp_cache_decr($key, $offset = 1, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->decr($key, $offset, $group);
  }
  function wp_cache_delete($key, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->delete($key, $group);
  }
  function wp_cache_flush() {
    global $wp_object_cache;
    return $wp_object_cache->flush();
  }
  function wp_cache_get($key, $group = '', $force = false, &$found = null) {
    global $wp_object_cache;
    return $wp_object_cache->get($key, $group, $force, $found);
  }
  function wp_cache_incr($key, $offset = 1, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->incr($key, $offset, $group);
  }
  function wp_cache_init() {
    if (function_exists('apcu_fetch')) {
      //APCuオブジェクトキャッシュのインスタンスを作成
      $GLOBALS['wp_object_cache'] = new APCu_Object_Cache();
    }
  }
  function wp_cache_replace($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->replace($key, $data, $group, (int) $expire);
  }
  function wp_cache_set($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->set($key, $data, $group, (int) $expire);
  }
  function wp_cache_switch_to_blog($blog_id) {
    global $wp_object_cache;
    $wp_object_cache->switch_to_blog($blog_id);
  }
  function wp_cache_add_global_groups($groups) {
    global $wp_object_cache;
    $wp_object_cache->add_global_groups($groups);
  }
  function wp_cache_add_non_persistent_groups($groups) {
    global $wp_object_cache;
    $wp_object_cache->wp_cache_add_non_persistent_groups($groups);
  }
  function wp_cache_reset() {
    global $wp_object_cache;
    $wp_object_cache->reset();
  }
  class APCu_Object_Cache {
    //APCuオブジェクトキャッシュの使用
    private $prefix = '';
    private $local_cache = array();
    private $global_groups = array();
    private $non_persistent_groups = array();
    private $multisite = false;
    private $blog_prefix = '';
    public function __construct() {
      global $table_prefix, $blog_id;
      $this->multisite = is_multisite();
      $this->blog_prefix =  $this->multisite ? intval($blog_id) : '';
      $this->prefix = DB_HOST . '.' . DB_NAME . '.' . $table_prefix;
    }
    private function get_group($group) {
      return empty($group) ? 'default' : $group;
    }
    private function get_key($group, $key) {
      if ($this->multisite && !isset($this->global_groups[$group])) {
        return $this->prefix . '.' . $group . '.' . $this->blog_prefix . ':' . $key;
      } else {
        return $this->prefix . '.' . $group . '.' . $key;
      }
    }
    public function add($key, $data, $group = 'default', $expire = 0) {
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (function_exists('wp_suspend_cache_addition') && wp_suspend_cache_addition()) {
        return false;
      }
      if (isset($this->local_cache[$group][$key])) {
        return false;
      }
      //FIXME: Somehow apcu_add does not return false if key already exists
      if (!isset($this->non_persistent_groups[$group]) && apcu_exists($key)) {
        return false;
      }
      if (is_object($data)) {
        $this->local_cache[$group][$key] = clone $data;
      } else {
        $this->local_cache[$group][$key] = $data;
      }
      if (!isset($this->non_persistent_groups[$group])) {
        return apcu_add($key, $data, (int) $expire);
      }
      return true;
    }

    //グローバルなキャッシュグループを追加
    public function add_global_groups($groups) {
      //グローバルグループの追加の使用
      if (is_array($groups)) {
        foreach ($groups as $group) {
          $this->global_groups[$group] = true;
        }
      } else {
        $this->global_groups[$groups] = true;
      }
    }
    public function wp_cache_add_non_persistent_groups($groups) {
      if (is_array($groups)) {
        foreach ($groups as $group) {
          $this->non_persistent_groups[$group] = true;
        }
      } else {
        $this->non_persistent_groups[$groups] = true;
      }
    }
    public function decr($key, $offset = 1, $group = 'default') {
      if ($offset < 0) {
        return $this->incr($key, abs($offset), $group);
      }
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (isset($this->local_cache[$group][$key]) && $this->local_cache[$group][$key] - $offset >= 0) {
        $this->local_cache[$group][$key] -= $offset;
      } else {
        $this->local_cache[$group][$key] = 0;
      }
      if (isset($this->non_persistent_groups[$group])) {
        return $this->local_cache[$group][$key];
      } else {
        $value = apcu_dec($key, $offset);
        if ($value < 0) {
          apcu_store($key, 0);
          return 0;
        }
        return $value;
      }
    }
    public function delete($key, $group = 'default', $force = false) {
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      unset($this->local_cache[$group][$key]);
      if (!isset($this->non_persistent_groups[$group])) {
        return apcu_delete($key);
      }
      return true;
    }
    public function flush() {
      $this->local_cache = array();
      //APCuのキャッシュエントリをクリアする
      apcu_clear_cache();
      return true;
    }
    public function get($key, $group = 'default', $force = false, &$found = null) {
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (!$force && isset($this->local_cache[$group][$key])) {
        $found = true;
        if (is_object($this->local_cache[$group][$key])) {
          return clone $this->local_cache[$group][$key];
        } else {
          return $this->local_cache[$group][$key];
        }
      } elseif (isset($this->non_persistent_groups[$group])) {
        $found = false;
        return false;
      } else {
        $value = apcu_fetch($key, $found);
        if ($found) {
          if ($force) {
            $this->local_cache[$group][$key] = $value;
          }
          return $value;
        } else {
          return false;
        }
      }
    }
    public function incr($key, $offset = 1, $group = 'default') {
      if ($offset < 0) {
        return $this->decr($key, abs($offset), $group);
      }
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (isset($this->local_cache[$group][$key]) && $this->local_cache[$group][$key] + $offset >= 0) {
        $this->local_cache[$group][$key] += $offset;
      } else {
        $this->local_cache[$group][$key] = 0;
      }
      if (isset($this->non_persistent_groups[$group])) {
        return $this->local_cache[$group][$key];
      } else {
        $value = apcu_inc($key, $offset);
        if ($value < 0) {
          apcu_store($key, 0);
          return 0;
        }
        return $value;
      }
    }
    public function replace($key, $data, $group = 'default', $expire = 0) {
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (isset($this->non_persistent_groups[$group])) {
        if (!isset($this->local_cache[$group][$key])) {
          return false;
        }
      } else {
        if (!isset($this->local_cache[$group][$key]) && !apcu_exists($key)) {
          return false;
        }
        apcu_store($key, $data, (int) $expire);
      }
      if (is_object($data)) {
        $this->local_cache[$group][$key] = clone $data;
      } else {
        $this->local_cache[$group][$key] = $data;
      }
      return true;
    }
    public function reset() {
      //This function is deprecated as of WordPress 3.5
      //Be safe and flush the cache if this function is still used
      $this->flush();
    }
    public function set($key, $data, $group = 'default', $expire = 0) {
      $group = $this->get_group($group);
      $key = $this->get_key($group, $key);
      if (is_object($data)) {
        $this->local_cache[$group][$key] = clone $data;
      } else {
        $this->local_cache[$group][$key] = $data;
      }
      if (!isset($this->non_persistent_groups[$group])) {
        return apcu_store($key, $data, (int) $expire);
      }
      return true;
    }
    public function stats() {
      //Only implemented because the default cache class provides this.
      //This method is never called.
      echo '';
    }
    public function switch_to_blog($blog_id) {
      $this->blog_prefix = $this->multisite ? intval($blog_id) : '';
    }
  }
}

キャッシュのクリアトリガー・クリア設定
キャッシュは定期的にクリアする必要があります。以下のコードをfunctions.phpに追加することで、キャッシュのクリアを制御することができます。

php


<?php //(functions.php)

/* 永続オブジェクトキャッシュ(APCu)
-------------------------------------- */
//最後のクリアから一定時間経過後にクリアするように(過度のクリア防止)
define('CACHE_CLEAR_INTERVAL', 5); //5秒

function should_clear_cache() {
  $last_cleared = get_option('last_cache_cleared_time', 0);
  return time() - $last_cleared > CACHE_CLEAR_INTERVAL;
}

//APCuキャッシュクリア関数
function clear_apcu_cache() {
  if (function_exists('apcu_clear_cache')) {
    apcu_clear_cache();
  } elseif (function_exists('apc_clear_cache')) {
    apc_clear_cache();
  }
  update_option('last_cache_cleared_time', time());
}

//記事投稿・更新時のAPCuキャッシュのクリア
function my_save_post($post_id, $post, $update) {
  // リビジョンやオートドラフトはスキップ
  if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
    return;
  }

  //APCuキャッシュのクリア
  if (should_clear_cache()) {
    clear_apcu_cache();
  }
}
add_action('save_post', 'my_save_post', 10, 3);

//メニュー更新時のAPCuキャッシュのクリア
function clear_cache_on_menu_update($menu_id, $menu_data) {
  if (should_clear_cache()) {
    clear_apcu_cache();
  }
}
add_action('wp_update_nav_menu', 'clear_cache_on_menu_update', 10, 2);

//カスタマイザ更新時のAPCuキャッシュのクリア
function clear_cache_on_customize_save($wp_customize) {
  if (should_clear_cache()) {
    clear_apcu_cache();
  }
}
add_action('customize_save_after', 'clear_cache_on_customize_save');

まとめ
永続オブジェクトキャッシュ(APCu)は、WordPressのパフォーマンス向上に有効な手段の一つです。上記の手順に従って設定することで、サイトの応答速度を向上させることが期待できます。ぜひ、試してみてください。

以上、WordPressでの永続オブジェクトキャッシュ(APCu)の使用方法についてのまとめでした。次回も役立つ情報をお届けしますので、お楽しみに!