2009/10/26

conditionalCacheFilterをさらに考える

はじめに

前回、conditionalCacheを説明した後に、さらにそれを使った事例を考えていたのですが、考えていたところ直したいところがいくつか出てきたので、さらにconditionalCacheを考えてみました。一つは、enabledオプションによって、キャッシュをクリアすることは必要ないと考えたことです。本来の動作としては、enabledがfalseであれば、キャッシュを使わずに、そのまま表示するという方法を取るのが一番でしょうね。これはviewCacheManagerのisCacheableメソッドを直せば可能となると思います。とりあえずその方法は後に説明するということで、今回はenabledオプションを消しました。
そして、今回は、セッション関係を持つuserクラスを使って、キャッシュをするか、どうかを追加してみることにしましたので、それを説明しようと思います。

問題

ユーザ認証しているかどうかだけではなくて、ユーザ認証して、かつ自分のコントロールできるページか否かというオプションがあったらウレシイですよね。つまり、そのユーザの投稿やプロフィールページであれば、「編集する」や「削除する」というアクションが表示されるのが普通でしょう。ユーザ認証をしていても、他のユーザの内容であれば、編集や削除ができたら困りますよね。ここでもconditionalCacheの出番が必要ではないか、と考えました。

方法

セッションを操るuserを使いましょう。filters.ymlで条件としてuser_conditionを追加してみます。そこで、user_conditionに、キャッシュ可能かどうかを条件判断させるメソッドを文字列で渡してみます。

ソースコード

/apps/frontend/config/filters.yml

rendering: ~
security:  ~

# insert your own filters here
conditionalCacheFilter:
  class: conditionalCacheFilter
  param:
    pages:
      - { module: post, action: index, format: [xml] }
#      - { module: post, action: index, format: [html], user_condition: usesCache }
      - { module: post, action: index, format: [html], user_condition: isNotAuthenticated, with_layout: true }

cache:     ~
execution: ~

/apps/frontend/lib/conditonalCacheFilter.class.php

class conditionalCacheFilter extends sfFilter
{
  protected
    $cacheManager    = null,
    $request         = null,
    $user            = null,
    $uri             = null,
    $defaultLifetime = null;

  public function initialize($context, $parameters = array())
  {
    parent::initialize($context, $parameters);

    $this->cacheManager    = $context->getViewCacheManager();
    $this->request         = $context->getRequest();
    $this->user            = $context->getUser();
    $this->uri             = $context->getRouting()->getCurrentInternalUri();

    $lifetime = (isset($this->cacheManager)) ? $this->cacheManager->getLifeTime($this->uri) : 0;
    $this->defaultLifetime = ($lifetime > 0) ? $lifetime : 86400;
  }

  public function execute($filterChain)
  {
    if ((!isset($this->cacheManager)) or !($this->getParameter('pages', false))) {
      $filterChain->execute();
      return;
    }

    foreach ($this->getParameter('pages') as $page) {

      $module = isset($page['module']) ? $page['module'] : null;

      $action = isset($page['action']) ? $page['action'] : $this->request->getParameter('action');

      $format = isset($page['format']) ? $page['format'] : array();
      if (!is_array($format)) {
        $format = array($format);
      }

      $user_condition = isset($page['user_condition']) ? $page['user_condition'] : null;

      // maybe better to throw exception?
      if (is_null($module)) {
        continue;
      }
      // skip
      if ($module !== $this->request->getParameter('module')) {
        continue;
      }
      // skip
      if (!empty($format) and !in_array($this->request->getParameter('sf_format', 'html'), $format)) {
        continue;
      }

      // skip
      if (isset($user_condition)) {
        $func = array($this->user, $user_condition);
        if (!(is_callable($func) and call_user_func($func, $this->request))) {
          continue;
        }
      }

      $options['lifeTime'] = isset($page['lifetime']) ? $page['lifetime'] : $this->defaultLifetime;
      // if the page is not cached already, you can set with_layout option
      // xxx symfony page cache cannot have with_layout and without_layout cache same time
      if (!$this->cacheManager->isCacheable($this->uri)) {
        $options['withLayout'] = isset($page['with_layout']) ? $page['with_layout'] : false;
      }

      //add
      $this->cacheManager->addCache($module, $action, $options);
    }

    // chain
    $filterChain->execute();
  }
}

/apps/frontend/lib/myUser.class.php

class myUser extends sfBasicSecurityUser
{
// snip some methods for user signin or signout methods.
//
  public function getUsername()
  {
    if ($this->isAuthenticated()) {
      return $this->getAttribute('username', '', 'test');
    }
    return null;
  }

  public function usesCache($request)
  {
    $username = $request->getParameter('username', false);
    return $this->getUsername() !== $username;
  }

  public function isNotAuthenticated()
  {
    return !$this->isAuthenticated();
  }
}

解説

今回は、userクラスで条件判断をつけるuser_conditionのキーをfilters.ymlで選択させるようにしてさらに柔軟にしていました。つまり、user_conditionに与えるメソッドによって、キャッシュするかどうかを変更することができます。ここでは、isNotAuthenticatedによって、認証されていないユーザにはキャッシュをするというように与えています。前回のis_authenticatedオプションではなく、直接userクラスに判断させるようにしてあります。user_conditionが指定していなければ、認証していようが、いまいが、キャッシュが有効になります。そして、認証している際にさらに条件を加えたい場合は、user_conditionでもう少し詳しい内容を実装したメソッドを指定してみましょう。

例えば、user_conditionにusesCacheを指定すれば、リクエストパラメターにusernameがあった際に、セッションに入っているusername(ログインした際に持っておく)と比較をし、同じものであれば(そのユーザに関するページ)、キャッシュをしない、ということも可能となります。

さて、with_layoutオプションですが、これは実は少し厄介なのです。現在のsymfonyでは、同じページのキャッシュを複数持つことができないのですね。つまり、with_layoutとwithout_layoutのキャッシュが同時に持てずに、切り分けることができないのです。実際は、キャッシュといっても、シリアライズされた内容をキャッシュディレクトリに格納しているのですが、with_layoutの方は、オブジェクトなのに対し、without_layoutの方はarrayでシリアライズされているので、ここで問題が起きてしまいます。これは、すでにcache.ymlでキャッシュを指定していた際のwith_layoutの値と違う値を指定するとエラーが起きてしまうので、このようにすでになければ指定可能だが、すでにあれば、with_layoutオプションは使えない、というようにしました。

まとめ

前回の内容からさらにconditionalCacheを考えて、userクラスのメソッドにそれを判断させるキーをfilters.ymlに追加しました。このことにより、認証済みでも、さらに自分に関するページに関してはキャッシュをしない、等の処理が可能となりました。つまり、認証していても他のユーザのページであれば、キャッシュを使えるということになります。

課題

キャッシュに関しては、まだ考えているところがあります。cache.ymlにて、enabledをtrueにしているが、conditionalCacheFilterに渡すenabledをfalseにした際に、キャッシュをクリアするのではなく、そのまま置いておいて、そのページはキャッシュを使わずに表示させたいと思います。また、パーシャルキャッシュについても考えた方が良いでしょう。前回と今回に関しましては、ページキャッシュを扱いましたが、パーシャルキャッシュもログイン状態によって切り分けたいときがあると思います。それに関しても考えていきたいと思います。

ちなみに、ここで私が載せている内容は、正しい唯一の方法ではなく、「現在の私だったら、こうやって実装するかな。」という内容です。なので、時間が経つに連れて、より良い方法が見つかった際には、そちらに移行すると思います。その際には、また、ブログに書くでしょう。

Leave a comment

Bloglines feedburner