symfony book 日本語ドキュメント

第12章 - キャッシュ

アプリケーションを加速する方法の1つは将来のリクエストのために生成されたHTMLコード、もしくは全ページのチャンク(塊)を保存することです。このテクニックはキャッシング(caching)という名前で知られ、サーバーサイドとクライアントサイドで管理できます。

symfonyは柔軟なサーバーキャッシュシステムを提供します。キャッシュによって、YAMLファイルに基づいたとても直感的なセットアップを通して、全レスポンス、アクションの結果、部分テンプレートもしくはテンプレートフラグメントをファイルに保存できるようにします。基本的なデータが変化するとき、コマンドラインもしくは、特別なアクションメソッドによってキャッシュの選択した部分を簡単にクリアできます。symfonyもHTTP 1.1のヘッダーを通してクライアントサイドのキャッシュをコントロールする簡単な方法を提供します。この章ではこれらすべての題目を扱い、キャッシュがアプリケーションにもたらす改善をモニタリングするいくつかのティップスを提示します。

レスポンスをキャッシュする

HTMLのキャッシュの原則はシンプルです: ユーザーに送信されるHTMLのコードの一部もしくはすべてが似たようなリクエストに対して再利用できます。このHTMLのコードは特別な場所(symfonyのcache/フォルダー)に保存されます。ここでフロントコントローラーはアクションを実行するまえにこのコードを探します。キャッシュされたバーションが見つかる場合、アクションを実行しないでこのコードが送られるので、処理速度が大いに加速します。キャッシュが見つからない場合、アクションが実行され、将来のリクエストのために結果(ビュー)がキャッシュ用のフォルダーに保存されます。

すべてのページが動的な情報を含むので、デフォルトではHTMLのキャッシュは無効です。パフォーマンスを改善するために有効にするかどうかはサイトの管理者次第です。

symfonyは3つの異なるタイプのHTMLキャッシュを扱います:

最初の2つのタイプはYAMLの設定ファイルで扱われます。テンプレートフラグメントのキャッシュはテンプレートのヘルパー関数の呼び出しによって管理されます。

グローバルキャッシュの設定

プロジェクトのそれぞれのアプリケーションのために、settings.ymlファイルのキャッシュ設定において、HTMLキャッシュメカニズムは環境ごとに有効、もしくは無効(デフォルト)にできます。リスト12-1はキャッシュの有効のお手本を示しています。

リスト12-1 - キャッシュを有効にする(frontend/config/settings.yml)

dev:
  .settings:
    cache:                  on

アクションをキャッシュする

静的な情報(データベースに依存しないもしくはセッションに依存するデータ)を表示するアクション、もしくは修正なしでデータベースから情報を読みとるアクション(典型的なものはGETリクエスト)はしばしばキャッシュのために理想的です。図12-1はこの場合においてページの要素がキャッシュされることを示しています: アクションの結果(そのテンプレート)もしくはレイアウトとのアクションの結果です。

図12-1 - アクションをキャッシュする

アクションをキャッシュする

たとえば、Webサイトのすべてのユーザーのリストを返すuser/listアクションを考えてください。ユーザーが修正、追加、もしくは除去(そしてこのことが"キャッシュから項目を除去する"のセクションで検討されます)されないかぎり、このリストはつねに同じ情報を示しますので、キャッシュのためのよい候補です。

キャッシュの有効と設定、アクションによるアクションはモジュールのconfig/ディレクトリに設置されたcache.ymlファイルのなかで定義されます。例としてリスト12-2をご覧ください。

リスト12-2 - アクションのためにキャッシュを有効にする(frontend/modules/user/config/cache.yml)

list:
  enabled:     on
  with_layout: false   # デフォルト値
  lifetime:    86400   # デフォルト値

この設定は、キャッシュがlistアクションのためにオンになっていること、およびレイアウトがアクション(デフォルトのふるまい)によってキャッシュされないことを保証します。このことは、キャッシュ済みのアクションが存在する場合でも、(部分テンプレートとコンポーネントと一緒に)レイアウトは再度処理されることを意味します。with_layout設定が`trueに設定された場合は、レイアウトはアクションによってキャッシュされ、再度処理されることはありません。

キャッシュの設定をテストするには、ブラウザーから開発環境のアクションを呼び出します。

http://myapp.example.com/frontend_dev.php/user/list

ページ内のアクションの領域周辺の境界に注目してください。最初に、領域は青色のヘッダーで、ページがキャッシュから由来するものではないことを示します。ページをリフレッシュすると、アクションの領域が黄色のヘッダーを持ち、キャッシュから由来するものであることを表します(レスポンスの時間単位で目立って加速されます)。後の章でキャッシュのテストとモニタリングを行う方法について詳しく学ぶことになります。

NOTE スロットはテンプレートの一部で、アクションをキャッシュすることはこのアクションのテンプレートのなかで定義されたスロットの値を保存することにもなります。ですのでキャッシュはスロットに対してネイティブに機能します。

キャッシュシステムは引数を持つページに対しても機能します。userモジュールが、たとえば、ユーザーの詳細を表示するためにid引数を必要とするshowアクションを持つとします。リスト12-3で示されるように、このアクションに対してキャッシュを有効にするにはcache.ymlファイルを修正します。

cache.ymlの内容を見やすくするには、リスト12-3でも示されるように、all:キーの下でモジュールのすべてのアクションのための設定を再編成することができます。

リスト12-3 - cache.ymlの全内容の例(frontend/modules/user/config/cache.yml)

list:
  enabled:    on
show:
  enabled:    on

all:
  with_layout: false   # デフォルト値
  lifetime:    86400   # デフォルト値

user/showアクションを異なるid引数で呼び出すたびにキャッシュ内で新しいレコードが作られます。ですので、このためのキャッシュ:

http://myapp.example.com/user/show/id/12

はつぎのキャッシュとは異なります:

http://myapp.example.com/user/show/id/25

CAUTION POSTメソッドもしくはGETパラメーターによって呼び出されたアクションはキャッシュされません。

with_layout設定は少し説明が必要です。この設定はどの種類のデータがキャッシュに保存されるのか実際に決定します。レイアウトなしのキャッシュに対しては、テンプレートの実行結果とアクション変数がキャッシュに保存されます。レイアウトありのキャッシュに対しては、レスポンスオブジェクト全体が保存されます。レイアウトありのキャッシュはレイアウトなしのキャッシュよりもずっと速いことを意味します。

機能的に余裕があれば(つまりレイアウトがセッションデータに依存しない場合)、レイアウトありのキャッシュを選ぶべきです。不幸なことに、レイアウトは動的な要素(たとえば、接続したユーザーの名前)を含むことが多いので、レイアウトなしのアクションのキャッシュはもっとも共通した設定です。しかしながら、RSSフィード、ポップアップとCookieに依存しないページはレイアウトありでキャッシュできます。

部分テンプレート、コンポーネント、もしくはコンポーネントスロットをキャッシュする

7章では、include_partial()ヘルパーを利用して、いくつかのテンプレートにまたがるコードのフラグメントを再利用する方法を説明しました。部分テンプレートはアクションとしてキャッシュすることが簡単で、図12-2で示されるように、キャッシュの有効方法は同じルールに従います。

図12-2 - 部分テンプレート、コンポーネント、コンポーネントスロットをキャッシュする

部分テンプレート、コンポーネント、コンポーネントスロットをキャッシュする

たとえば、リスト12-4はuserモジュールに設置された_my_partial.php上でキャッシュを有効にするためにcache.ymlファイルを編集する方法を示しています。この場合においてwith_layout設定は意味を成さないことを覚えておいてください。

リスト12-4- 部分テンプレートのキャッシュ設定(frontend/modukes/user/config/cache.yml)

_my_partial:
  enabled:    on
list:
  enabled:    on
...

この部分テンプレートを利用するすべてのテンプレートは実際には部分テンプレートのPHPコードを実行しませんが、代わりにキャッシュバーションを使います。

[php]
<?php include_partial('user/my_partial') ?>

アクションに関しては、部分テンプレートの結果がパラメーターに依存するときに部分テンプレートキャッシュも該当します。キャッシュシステムはパラメーターの異なる値と同じ数のバージョンのテンプレートを保存します。

[php]
<?php include_partial('user/my_other_partial', array('foo' => 'bar')) ?>

TIP アクションのキャッシュは部分テンプレートのキャッシュよりもはるかに強力です。アクションがキャッシュされたとき、テンプレートの実行でさえも行われないからです; テンプレートが部分テンプレートの呼び出しを含む場合、これらの呼び出しは実行されません。それゆえ、部分テンプレートのキャッシュは、アクションを呼び出す際もしくはレイアウトに含まれる部分テンプレートに対してアクションのキャッシュを使わない場合のみ便利です。

7章を少し思い出してみましょう: コンポーネントは部分テンプレートのトップに設置される軽量のアクションで、コンポーネントスロットはアクションを呼び出すことにしたがって変化するアクションのためのコンポーネントです。これら2つのインクルージョンのタイプは部分テンプレートとよく似ており、同じ方法でキャッシュをサポートします。たとえば、現在の日時を表示するために、グローバルレイアウトがinclude_component('general/day')dayと呼ばれているコンポーネントをインクルードする場合、このコンポーネント上のキャッシュを有効にするにはgeneralモジュールのcache.ymlファイルをつぎのように設定します:

_day:
  enabled: on

コンポーネントもしくは部分テンプレートをキャッシュするとき、呼び出しているすべてのテンプレートに対してキャッシュの単独バージョンか、それぞれのテンプレートに対するバージョンを保存するか決めなければなりません。デフォルトでは、コンポーネントはそれを呼び出すテンプレートに対して独立して保存されます。しかしながら文脈依存のコンポーネント、たとえばそれぞれのアクションで異なるサイドバーを表示するコンポーネント、はテンプレートがそれを呼び出す回数と同じ回数だけ保存されます。つぎのようにcontextualパラメーターをtrueに設定することを前提にする場合、キャッシュシステムはこの事例を扱うことができます:

_day:
  contextual: true
  enabled:   on

NOTE アプリケーションのcache.ymlのなかでキャッシュの設定を宣言することを前提にする場合、グローバルコンポーネント(アプリケーションのtemplatesディレクトリに設置される)をキャッシュできます。

テンプレートのフラグメントをキャッシュする

アクションのキャッシュはアクションの部分集合のみに適用されます。ほかのアクションに対して、データを更新する、もしくはテンプレートでセッションに依存する情報を表示するアクションなど、は異なる方法ですがまだキャッシュの改善の余地はあります。symfonyは3つのキャッシュタイプを提供します。これらはテンプレートのフラグメント専用でテンプレート内部で直接有効にできます。このモードにおいて、図12-3で示されるように、アクションはつねに実行され、テンプレートは実行されたフラグメントとキャッシュのフラグメントに分割されます。

図12-3 テンプレートのフラグメントをキャッシュする

テンプレートのフラグメントをキャッシュする

たとえば、最後にアクセスしたユーザーのリンクを表示するユーザーのリストがある場合、この情報は動的です。cache()ヘルパーはキャッシュに保存されたテンプレートの部分を定義します。リスト12-5で構文の詳細をご覧ください。

リスト12-5 - cache()ヘルパーを使う(frontend/modules/user/templates/listSuccess.php)

[php]
<!-- Code executed each time -->
<?php echo link_to('last accessed user', 'user/show?id='.$last_accessed_user_id) ?>

<!-- Cached code -->
<?php if (!cache('users')): ?>
  <?php foreach ($users as $user): ?>
    <?php echo $user->getName() ?>
  <?php endforeach; ?>
  <?php cache_save() ?>
<?php endif; ?>

つぎのように動作します:

このような行が含まれないコードはつねに処理され、キャッシュされません。

CAUTION アクション(この例ではlist)はキャッシュを有効にしてはなりません。これはテンプレート全体の実行を回避し、フラグメントのキャッシュの宣言を無視するからです。

テンプレートフラグメントのキャッシュの使用による加速はアクションのキャッシュによるものよりは重要ではありません。アクションはつねに実行され、テンプレートは部分的に処理され、レイアウトはつねにデコレーションのために使われるからです。

追加のフラグメントを同じテンプレートのなかで宣言できます; しかしながら、それらに対して個別にユニークな名前をつけることで、symfonyのキャッシュシステムはこれらをあとで見つけることができます。

アクションとコンポーネントに関しては、キャッシュされたフラグメントはcache()ヘルパーへの呼び出しの2番目の引数として秒単位の期限を持ちます。

[php]
<?php if (!cache('users', 43200)): ?>

パラメーターがヘルパーに渡されない場合、デフォルトのキャッシュの期限(86400秒もしくは1日)が使われます。

TIP アクションをキャッシュできるようにする別の方法はアクションのルーティングパターンに変化する変数を挿入することです。たとえば、ホームページが接続ユーザーの名前を表示する場合、URLがユーザーのニックネームを含まないかぎり、キャッシュできません。ほかの例は国際化アプリケーションに関するものです: いくつかの翻訳を持つページの上でキャッシュを有効にしたい場合、言語コードを何とかしてURLパターンに含めなければなりません。このトリックはキャッシュにおけるページ数を増やしますが、インタラクティブなアプリケーションを大きく加速するために大いに役立ちます。

キャッシュを動的に設定する

cache.ymlファイルはキャッシュの設定を定義する1つの方法ですが、不変であるが故に不便です。しかしながら、symfonyにおいていつものとおり、YAMLよりも無地のPHPを利用できるので、キャッシュを動的に設定できます。

キャッシュの設定を動的に変更したいのはなぜでしょうか?よい例は認証ユーザーと匿名ユーザーのページの内容は異なるが、URLは同じであるページです。記事に対して評価システムを持つarticle/showページを想像してください。評価機能は匿名ユーザーに対して無効です。これらのユーザーに対しては、リンクはログインフォームの表示を行います。このバージョンのページはキャッシュできます。一方で、認証ユーザーに対しては、評価リンクをクリックすることでPOSTリクエストと新しい評価が作成されます。今回、symfonyが動的にページをビルドするにはページに対してキャッシュを無効にしなければなりません。

動的なキャッシュの設定を定義する正しい場所はsfCacheFilterのまえに実行されるフィルターです。本当に、セキュリティ機能と同じように、キャッシュはsymfonyのフィルターです。ユーザーが認証されていない場合だけarticle/showページに対してキャッシュを有効にするには、リスト12-6で示されるように、conditionalCacheFilterをアプリケーションのlib/ディレクトリのなかに作成します。

リスト12-6 - PHPを使ってキャッシュを設定する(frontend/lib/conditinalCacheFilter.class.php)

[php]
class conditionalCacheFilter extends sfFilter
{
  public function execute($filterChain)
  {
    $context = $this->getContext();
    if (!$context->getUser()->isAuthenticated())
    {
      foreach ($this->getParameter('pages') as $page)
      {
        $context->getViewCacheManager()->addCache($page['module'], $page['action'], array('lifeTime' => 86400));
      }
    }

    // つぎのフィルターを実行する
    $filterChain->execute();
  }
}

リスト12-7で示されるように、sfCacheFilterのまえにこのフィルターをfilters.ymlファイルに登録しなければなりません。

リスト12-7 - カスタムフィルターを登録する(frontend/config/filters.yml)

...
security: ~

conditionalCache:
  class: conditionalCacheFilter
  param:
    pages:
      - { module: article, action: show }

cache: ~
...

キャッシュをクリアすれば(新しいフィルタークラスをオートロードするため)、条件つきのキャッシュの準備ができます。これは、認証されていないユーザーに対してだけ、ページパラメーターで定義されたページのキャッシュを有効にします。

sfViewCacheManagerオブジェクトのaddCache()メソッドはモジュールの名前、アクションの名前、そしてcache.ymlファイルのなかで定義するパラメーターと同じ内容を持つ連想配列を必要とします。たとえば、article/showをレイアウトありで3600秒の期限の条件でキャッシュしなければならないことを定義したい場合、つぎのように書きます:

[php]
$context->getViewCacheManager()->addCache('article', 'show', array(
  'withLayout' => true,
  'lifeTime'   => 3600,
));

SIDEBAR 代替のキャッシュストレージ

デフォルトでは、symfonyのキャッシュシステムはWebサーバーのハードディスク上のファイルにデータを保存します。メモリ(たとえばmemcached経由)もしくはデータベース(いくつかのサーバー間でキャッシュを共有したいもしくはキャッシュの除去を加速したい場合)にキャッシュを保存したい場合を考えます。このためには、symfonyのデフォルトのキャッシュストレージシステムを簡単に変更できます。symfonyのビューキャッシュマネージャーによって使われるキャッシュクラスはfactories.ymlに定義されるからです。

デフォルトのビューキャッシュストレージファクトリはsfFileCacheクラスです:

view_cache:
    class: sfFileCache
    param:
      automaticCleaningFactor: 0
      cacheDir:                %SF_TEMPLATE_CACHE_DIR%

classを独自のキャッシュストレージクラスもしくはsymfonyの代替クラスの1つ(sfAPCCachesfEAcceleratorCachesfMemcacheCachesfSQLiteCache、とsfXCacheCacheを含む)に置き換えることができます。paramキーの下で定義されたパラメーターはあなたのクラスのinitialize()メソッドに連想配列として渡されます。どのビューキャッシュストレージクラスもsfCache抽象クラスのなかのメソッドをすべて実装しなければなりません。この主題に関する詳しい情報は19章を参照してください。

スーパーファーストキャッシュを利用する

キャッシュされたページでさえいくつかのPHPコードの実行をともないます。このようなページのために、symfonyは設定をロードし、レスポンスをビルドするなどの動作を行います。しばらくの間ページが変更されないことが本当にわかっている場合、HTMLコードの結果を直接web/フォルダーに設置することでsymfonyを完全に回避できます。ルーティングルールがサフィックスで終わらない、もしくは.htmlで終わるパターンを指定するという条件の下で、Apacheのmod_rewrite設定のおかげでこれは機能します。

シンプルなコマンドラインの呼び出しによって、ページ単位で、これを手作業で行うことができます:

> curl http://myapp.example.com/user/list.html > web/user/list.html

そのあとで、user/listアクションがリクエストされるたびに、Apacheは対応するlist.htmlページを見つけsymfonyを完全に回避します。トレードオフはもはやsymfonyでページキャッシュをコントロールできないことですが(期限、自動削除など)、速度のゲインは目覚ましいです。

代わりの方法として、sfSuperCacheプラグインを利用できます。このプラグインはこの処理を自動化して、期限とキャッシュのクリアをサポートします。プラグインに関して詳しい情報は17章を参照してください。

SIDEBAR ほかのスピードアップ戦術

HTMLのキャッシュに加えて、symfonyは他に2つのキャッシュメカニズムを持ちます。これらは完全に自動化され、開発者に対して透過的なものになります。運用環境において、設定とテンプレートの翻訳は介入なしでmyproject/cache/config/myproject/cache/i18n/ディレクトリに保存されたファイルにキャッシュされます。

PHPアクセレータ(eAccelerator、APC、XCacheなど)はオペレーションコードキャッシュモジュールとも呼ばれ、コンパイルされた状態のPHPスクリプトをキャッシュすることでパフォーマンスを増大させるので、コードの解析とコンパイルのオーバーヘッドは徹底的に削減されます。これは大量のコードを含むPropelのクラスに対してとりわけ効果的です。これらのアクセレータはsymfonyと互換性があり、簡単にアプリケーションの速度を3倍にします。これらは大規模な利用者を持つsymfonyアプリケーションに対して運用環境で推奨されます。

sfProcessCacheクラスによる、それぞれのリクエストに対して同じ処理を行うことを避けるために、PHPアクセレータで、永続的なデータを手動でメモリに保存できます。そしてCPUに負荷のかかる作業の結果をキャッシュしたい場合、おそらくsfFunctionCacheオブジェクトを使うことになります。これらのメカニズムに関して詳しい情報については18章を参照してください。

キャッシュから項目を除去する

アプリケーションのスクリプトもしくはデータが変化する場合、キャッシュは期限切れの情報を含みます。矛盾とバグを避けるために、あなたのニーズに合わせて、多くの異なる方法でキャッシュの部分を削除できます。

キャッシュ全体をクリアする

symfonyのコマンドラインのcache:clearタスクはキャッシュ(HTML、設定、ルーティングと国際化のキャッシュ)を削除します。リスト12-8で示されるように、キャッシュの部分集合だけを削除するためにこれを引数に渡すことができます。symfonyのプロジェクトのrootからのみ呼び出すことを覚えておいてください。

リスト12-8 - キャッシュをクリアする

// キャッシュ全体を消去する
> php symfony cache:clear

// 短い構文
> php symfony cc

// frontendアプリケーションのキャッシュのみを削除する
> php symfony cache:clear --app=frontend

// myappアプリケーションのHTMLキャッシュだけを削除する
> php symfony cache:clear --app=frontend --type=template

// frontendアプリケーションの設定のキャッシュのみを削除する
// 組み込みのタイプはconfig、i18n、routing、とtemplate。
> php symfony cache:clear --app=frontend --type=config

// frontendアプリケーションとprod環境のコンフィギュレーションキャッシュのみを削除する
> php symfony cache:clear --app=frontend --type=config --env=prod

NOTE Microsoft Windowsのもとでは、二重引用符でオプションを囲まなければならないことがあります:

> php symfony cache:clear "--app=frontend"

抜粋した部分のキャッシュをクリアする

データベースが更新されたとき、修正されたデータに関連するアクションのキャッシュをクリアしなければなりません。キャッシュ全体をクリアできましたが、モデルの変更に関係のない既存のすべてのキャッシュされたアクションに対しては無駄な作業です。これがsfViewCacheManagerオブジェクトのremove()メソッドを適用する事例です。これは引数として内部URIを必要とし(link_to()に提供する引数と同じ種類のもの)、関連するアクションのキャッシュを削除します。

たとえば、userモジュールのupdateアクションがUserオブジェクトのカラムを修正することを想像してください。キャッシュ済みのlistアクションとshowアクションをクリアすることが必要です。さもなければエラーのあるデータを含む古いバージョンが表示されます。これを処理するには、リスト12-9で示されるように、remove()メソッドを使います。

リスト12-9 - 任意のアクションに対してキャッシュをクリアする(modules/user/actions/actions.class.php)

[php]
public function executeUpdate($request)
{
  // ユーザーを更新する
  $user_id = $request->getParameter('id');
  $user = UserPeer::retrieveByPk($user_id);
  $this->foward404Unless($user);
  $user->setName($request->getParameter('name'));
  ...
  $user->save();

  // このユーザーに関連するアクションに対するキャッシュをクリアする
  $cacheManager = $this->getContext()->getViewCacheManager();
  $cacheManager->remove('user/list');
  $cacheManager->remove('user/show?id='.$user_id);
  ...
}

キャッシュされた部分テンプレート、コンポーネント、コンポーネントスロットを除去する作業は少々複雑です。任意のタイプのパラメーター(オブジェクトを含む)をこれらに渡すことができるので、あとでキャッシュバージョンを識別することが不可能だからです。部分テンプレートに焦点を当てて説明します。このしくみはほかのテンプレートのコンポーネントについても同様です。symfonyはキャッシュされた部分テンプレートを、特別なプレフィックス(sf_cache_partial)、モジュールの名前、部分テンプレートの名前、に加えて呼び出すために使われるすべてのパラメーターのハッシュで識別します:

[php]
// 呼び出された部分テンプレート
<?php include_partial('user/my_partial', array('user' => $user) ?>

// はつぎのようなキャッシュに分類される
@sf_cache_partial?module=user&action=_my_partial&sf_cache_key=bf41dd9c84d59f3574a5da244626dcc8

理論上では、識別するために使われるパラメーターのハッシュの値を知っているのであれば、キャッシュされた部分テンプレートをremove()メソッドで削除できます。しかし、これは現実的な方法ではありません。幸いにして、sf_cache_keyパラメーターをinclude_partial()ヘルパー呼び出しに追加する場合、あなたにとって既知のものとしてキャッシュのなかの部分テンプレートを識別できます。リスト12-10で見ることができるように、単独のキャッシュされた部分テンプレートをクリアする作業、たとえば、修正されたUserに基づいた部分テンプレートからキャッシュをクリーンアップする作業が簡単になります。

リスト12-10 - 部分テンプレートをキャッシュからクリアする

[php]
<?php include_partial('user/my_partial', array(
  'user'         => $user,
  'sf_cache_key' => $user->getId()
) ?>

//つぎのようにキャッシュに分類される
@sf_cache_partial?module=user&action=_my_partial&sf_cache_key=12

// つぎのコードでキャッシュの特定のユーザーに対する_my_partialをクリアする
$cacheManager->remove('@sf_cache_partial?module=user&action=_my_partial&sf_cache_key='.$user->getId());

テンプレートフラグメントをクリアするには、同じremove()メソッドを使います。キャッシュのフラグメントを指定するキーは同じsf_cache_partialのプレフィックス、モジュールの名前、アクションの名前とsf_cache_key/で構成されます(キャッシュフラグメントのユニークな名前はcache()ヘルパーに含まれます)。リスト12-11は例を示しています。

リスト12-11 - キャッシュから由来するテンプレートフラグメントをクリアする

[php]
<!-- Cached code -->
<?php if (!cache('users')): ?>
  ... // Whatever
  <?php cache_save() ?>
<?php endif; ?>

// つぎのようにキャッシュに分類される
@sf_cache_partial?module=user&action=list&sf_cache_key=users

// つぎのコードでクリアする
$cacheManager->remove('@sf_cache_partial?module=user&action=list&sf_cache_key=users');

SIDEBAR クリアするキャッシュの選択に悩む

cache-clearingジョブのもっとも扱いにくい部分はデータの更新によってどのアクションが影響されるのかを決定することです。

たとえば、現在のアプリケーションがpublicationモジュールを持つことを想像してください。そこでは著者の詳細情報(Userクラスのインスタンス)と一緒に、公開情報の一覧が表示され(listアクション)説明されます(showアクション)。1つのUserレコードの修正はユーザーの公開情報の説明と公開情報の一覧に影響を与えます。このことは、つぎのように、userモジュールのupdateアクションを追加する必要があることを意味します:

[php]
$c = new Criteria();
$c->add(PublicationPeer::AUTHOR_ID, $request->getParameter('id'));
$publications = PublicationPeer::doSelect($c);

$cacheManager = sfContext::getInstance()->getViewCacheManager();
foreach ($publications as $publication)
{
  $cacheManager->remove('publication/show?id='.$publication->getId());
}
$cacheManager->remove('publication/list');

HTMLのキャッシュを使い始めるとき、誤解されたリレーションが原因で新しいエラーが表示されないように、モデルとアクションの依存関係の展望をクリアに保つ必要があります。HTMLのキャッシュがアプリケーションのどこかで使われる場合、モデルを修正するすべてのアクションはおそらくremove()メソッドの一連の呼び出しを含むべきであることを覚えておいてください。

そして、難しすぎる解析に悩みたくないのであれば、データベースを更新するたびにキャッシュ全体をつねにクリアできます・・・

一度に複数のキャッシュ部分をクリアする(1.1の新しい機能)

remove()メソッドはワイルドカードを持つキーを受けとります。これによって単独の呼び出しでいくつかのキャッシュを削除できるようになります。たとえばつぎのように行うことができます:

[php]
$cacheManager->remove('user/show?id=*');    // 現在のホストのすべてのユーザーのレコードのキャッシュを削除する

別のよい例は、すべてのURLに言語コードが現れる、複数の言語を扱うアプリケーションです。ユーザーのプロファイルのページへのURLはつぎのようになります:

http://www.myapp.com/en/user/show/id/12

id12であるユーザーのプロファイルのキャッシュを削除するには、つぎのように簡単に呼び出すことができます:

[php]
$cache->remove('user/show?sf_culture=*&id=12');

これは部分テンプレートに対しても機能します:

[php]
$cacheManager->remove('@sf_cache_partial?module=user&action=_my_partial&sf_cache_key=*');    // すべてのキーに対して除去する

remove()メソッドは2つの追加パラメーターを受けとり、キャッシュをクリアしたいホストとvaryヘッダーを定義できるようにします。symfonyはそれぞれのホストとvaryヘッダーに対して1つのキャッシュバージョンを保存するので、同じコードベースを共有するが同じホスト名を共有しない2つのアプリケーションは異なるキャッシュを使います。これはアプリケーションがサブドメイン(たとえばhttp://php.askeet.comhttp://life.askeet.com)をリクエストパラメーターとして解釈するとき、とても便利です。最後の2つのパラメーターを設定しない場合、symfonyは現在のホストとallのvaryヘッダーに対してキャッシュを除去します。代わりに、別のホストに対してキャッシュを除去したい場合、つぎのようにremove()を呼び出します:

[php]
$cacheManager->remove('user/show?id=*');                     //現在のホストのすべてのユーザーのキャッシュレコードを削除する
$cacheManager->remove('user/show?id=*', 'life.askeet.com');  // life.askeet.comのホストのすべてのユーザーのキャッシュレコードを削除する
$cacheManager->remove('user/show?id=*', '*'); //すべてのホストのすべてのユーザーのキャッシュレコードを削除する

remove()メソッドはfactories.ymlで定義できるすべてのキャッシュストレージで機能します(sfFileCacheだけでなく、sfAPCCachesfEAcceleratorCachesfMemcacheCachesfSQLiteCache、 とsfXCacheCacheも).

複数のアプリケーションにまたがったキャッシュをクリアする(symfony 1.1の新しい機能)

複数のアプリケーションにまたがるキャッシュをクリアすることが問題になる可能性があります。たとえば、管理者がbackendアプリケーションのuserテーブルのレコードを修正する場合、frontendアプリケーションのこのユーザーに関係するすべてのアクションはキャッシュからクリアされる必要があります。しかし backendアプリケーションが利用できるビューキャッシュマネージャはfrontendアプリケーションのルーティングルールがわかりません(アプリケーションはそれぞれ独立しています)。ですのでbackendアプリケーションのなかで以下のコードを書くことはできません:

[php]
$cacheManager = sfContext::getInstance()->getViewCacheManager(); // backendアプリケーションのビューキャッシュマネージャを読みとる
$cacheManager->remove('user/show?id=12');                        // テンプレートはfrontendアプリケーション内に存在するので、パターンは見つからない

解決策は、frontendキャッシュマネージャと同じ設定で、手動でsfCacheオブジェクトを初期化することです。幸いにして、symfony内のすべてのキャッシュクラスはビューキャッシュマネージャのremoveと同じサービスを供与するremovePatternメソッドを提供します。

たとえば、backendアプリケーションがid12であるユーザーに対してfrontendアプリケーションのなかでuser/showアクションのキャッシュをクリアしたい場合、つぎのコードを利用できます:

[php]
$frontend_cache_dir = sfConfig::get('sf_root_cache_dir').DIRECTORY_SEPARATOR.'frontend'.DIRECTORY_SEPARATOR.SF_ENV.DIRECTORY_SEPARATOR.'template';
$cache = new sfFileCache(array('cache_dir' => $frontend_cache_dir)); //frontendのfactories.ymlで定義された設定と同じものを使う
$cache->removePattern('user/show?id=12');

異なるキャッシュ戦略に対して、キャッシュのオブジェクトの初期化を変更することだけが必要ですが、キャッシュの除去プロセスは変わりません:

[php]
$cache = new sfMemcacheCache(array('prefix' => 'frontend'));
$cache->removePattern('user/show?id=12');

キャッシュのテストとモニタリング

HTMLのキャッシュは、適切に処理されない場合、表示されるデータに矛盾が生じる可能性があります。要素に対してキャッシュを無効にするたびに、徹底的にテストし、調整するために実行ブーストのモニタリングを行います。

ステージング環境を作成する

開発環境ではHTMLのキャッシュはデフォルトで無効なので、運用環境のキャッシュシステムは開発環境で検出できない新しいエラーに陥りがちです。なんらかのアクションのためにHTMLのキャッシュを有効にする場合、このセクションではステージング(staging)と呼ばれる新しい環境を作ります。ステージング環境はprod環境と同じ設定(したがって、キャッシュが有効)ですが、web_debug設定はonです。

セットアップするには、アプリケーションのsettings.ymlファイルを編集してリスト12-12の一番上に示されている行を追加します。

リスト12-12 - frontend/config/settings.ymlのなかで、staging環境のための設定

staging:
  .settings:
    web_debug:  on
    cache:      on

加えて、運用環境のフロントコントローラー(おそらくmyproject/web/index.php)を新しいfrontend_staging.phpにコピーして新しいフロントコントローラーを作ります。つぎのようにgetApplicationConfiguration()メソッドに渡される引数を変更するためにこのファイルを編集します:

[php]
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'staging', true);

作業はこれだけです。新しい環境が手に入りました。ドメイン名の後にフロントコントローラー名を追加して使います:

http://myapp.example.com/frontend_staging.php/user/list

パフォーマンスをモニタリングする

16章でWebデバッグツールバーとその内容について深く掘り下げて説明します。しかしながら、このツールバーはキャッシュの要素に関して有益な情報を提供します。キャッシュ機能の手短な説明はつぎのとおりです。

キャッシュ可能な要素(アクション、部分テンプレート、フラグメントなど)を含むページをブラウザーで閲覧するとき、図12-4で示されるようにWebデバッグツールバー(ウィンドウの右上のコーナー)が「ignore cache button」(緑で丸まった矢印)を表示します。このボタンはページをリロードして、キャッシュされた要素の処理を強制します。キャッシュはこのボタンでクリアされないことを覚えておいてください。

デバッグツールバーの右側の最後の数値はリクエストの実行期間です。ページ上でキャッシュを有効にした場合、この数値はページをロードする秒数を減らします。symfonyがスクリプトを再処理する代わりにキャッシュからデータを使うからです。このインディケータによって簡単にキャッシュの改善をモニタリングできます。

図12-4 - キャッシュを使うページのためのWebデバッグツールバー

キャッシュを使うページのためのWebデバッグツールバー

デバッグツールバーはリクエストの処理の間に実行されたデータベースクエリの数も表示し、カテゴリごとの継続時間の詳細を示します(詳細を表示するtotal durationをクリックします)。このデータのモニタリングは、継続時間の合計と共に、キャッシュによってもたらされたすばらしいパフォーマンス改善の測定の助けになります。

ベンチマーキングを行う

多くの情報がWebデバッグツールバーで記録され利用可能になるので、デバッグモードはアプリケーションの速度を大きく減速させます。ですので、staging環境でブラウジングしているとき、表示される処理時間はデバッグモードがoffになっている運用環境で何が起きたのかを表していません。

それぞれのリクエストの処理時間のより優れた概要を知るには、Apache BenchやJMeterなどのベンチマーキングツールを使うべきです。これらのツールによってロードテストが可能になり2つの重要な情報の断片が提供されます: 2つの情報とは特定のページの平均的な読み込み時間とサーバーの最大キャパシティです。平均的な読み込み時間のデータはキャッシュの有効によるパフォーマンスの改善をモニタリングするためにとても役立ちます。

キャッシュの一部を識別する

Webデバッグツールが有効にされたとき、図12-5で示されるように、キャッシュされた要素はそれぞれがトップ左側のキャッシュ情報のボックスを持つ赤いフレームによるページで識別されます。要素が実行された場合はボックスの背景が青色になり、ページがキャッシュから由来する場合は背景が黄色になります。キャッシュ情報をクリックするとキャッシュ要素の識別子、期限と最終修正以降の経過時間が表示されます。これは、文脈外の要素を処理するとき、要素が作られた時と実際にキャッシュできるテンプレートの部分を理解するために、問題を識別するための助けになります。

図12-5 - ページのキャッシュされた要素を識別する

ページのキャッシュされた要素を識別する

HTTP 1.1とクライアントサイドのキャッシュ

HTTP 1.1プロトコルはブラウザーのキャッシュシステムをコントロールすることでアプリケーションを加速するために大いに役立つ一連のヘッダーを定義します。

World Wide Web Consortium(W3C, http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)のHTTP 1.1の仕様はこれらのヘッダーを詳細に説明をしています(訳注:rfc2616の日本語訳)。アクションがキャッシュを有効にしていて、with_layoutオプションを使っている場合、アクションはつぎのセクションで説明される1つもしくは複数のメカニズムを利用します。

Webサイトのユーザーのブラウザーの一部がHTTP 1.1をサポートしていなかったとしても、HTTP 1.1のキャッシュ機能を使うことに関してリスクは存在しません。簡単に理解できないヘッダーを受けとるブラウザーはこれらの機能を無視するので、HTTP 1.1のキャッシュメカニズムをセットアップすることをお勧めします。

加えて、HTTP 1.1のヘッダーはプロキシサーバーとキャッシュサーバーも理解できます。ユーザーのブラウザーがHTTP 1.1を理解しない場合でも、それを利用するリクエストのroute内部でプロキシが存在できます。

変更されていない内容の送信を回避するためにETagヘッダーを追加する

ETag機能が有効な場合、Webサーバーはレスポンス自身の署名を含む特別なヘッダーをレスポンスに追加します。

ETag: "1A2Z3E4R5T6Y7U"

ユーザーのブラウザーはこの署名を保存し、同じページが必要になる次回にリクエストと一緒に再び送信します。新しい署名が最初のリクエスト以降にページが変更されていないことを示す場合、サーバーはレスポンスを返信しません。代わりに、304: Not modifiedヘッダーに送信できます。CPUの時間(この例ではgzipが有効)とサーバーのための帯域(ページの転送)とクライアントのための時間(ページの転送)が節約されます。全体で、ETagありのキャッシュ内のページはETagなしのキャッシュ内のページよりもロードするのが速いです。

symfonyにおいて、アプリケーション全体に対してsettings.ymlのなかでETag機能を有効にします。デフォルトのETag設定はつぎのとおりです:

all:
  .settings:
    etag: on

レイアウトありのキャッシュにおけるアクションは、cache/ディレクトリから直接レスポンスを取得するので処理がずっと高速になります。

まだ有効な内容の送信を回避するためにLast-Modifiedヘッダーを追加する

サーバーがレスポンスをブラウザーに送信するとき、ページに含まれるデータが最後に更新された時間を指定するために特別なヘッダーを追加できます:

Last-Modified: Sat, 23 Nov 2006 13:27:31 GMT

ブラウザーはこのヘッダーを理解できるので、再びページをリクエストするとき、それに合わせてIf-Modifiedヘッダーを追加します:

If-Modified-Since: Sat, 23 Nov 2006   13:27:31 GMT

サーバーはクライアントの値とアプリケーションによって返される値を比較できます。これらがマッチする場合、サーバーは304: Not modifiedヘッダーを返し、ETagと同じように帯域とCPUの時間を節約します。

symfonyにおいて、別のヘッダーに対して行うことと同じように、Last-Modified応答ヘッダーを設定できます。たとえば、これをアクションのなかで利用できます:

[php]
$this->getResponse()->setHttpHeader('Last-Modified', $this->getResponse()->getDate($timestamp));

この日付は、データベースもしくはファイルシステムから渡される、ページで使われるデータの実際の最終更新日になります。sfResponseオブジェクトのgetDate()メソッドはタイムスタンプをLast-Modifiedヘッダー(RFC1123)のために必要な書式の日付に変換します。

キャッシュ済みのページを利用可能にするVaryヘッダーを追加する

HTTP 1.1の別のヘッダーはVaryです。このヘッダーはページが依存するパラメーターがどれなのかを定義し、キャッシュのキーを作成するためにブラウザーとプロキシによって使われます。たとえば、ページの内容がCookieに依存する場合、Varyヘッダーをつぎのように設定できます:

Vary: Cookie

たいていの場合、アクション上でキャッシュを有効にすることは難しいです。なぜならページがCookie、ユーザーの言語、もしくはその他にしたがって変わるかもしれないからです。キャッシュのサイズを増やすことをいとわないのであれば、レスポンスのVaryヘッダーを適切に設定してください。つぎのようなcache.yml設定ファイルもしくはメソッドに関連するsfResponseを使うことで、この設定はアプリケーション全体のため、もしくはアクションをベースに行うことができます。:

[php]
$this->getResponse()->addVaryHttpHeader('Cookie');
$this->getResponse()->addVaryHttpHeader('User-Agent');
$this->getResponse()->addVaryHttpHeader('Accept-Language');

symfonyはこれらのパラメーターの値ごとに異なるバージョンのページをキャッシュに保存します。これはキャッシュのサイズを増やしますが、サーバーがこれらのヘッダーにマッチするリクエストを受けとるたびに、レスポンスは処理される代わりにキャッシュからとり込まれます。リクエストのヘッダーだけにしたがって変化するページのための偉大なパフォーマンスツールです。

クライアントサイドのキャッシュを可能にするCache-Controlヘッダーを追加する

これまで、ヘッダーを追加することによって、キャッシュ済みのページ保持する場合でも、ブラウザーはサーバーへのリクエストの送信を続けました。Cache-ControlヘッダーとExpiresヘッダーをレスポンスに追加することでこれを避けることができます。PHPにおいてこれらのヘッダーはデフォルトで無効ですが、サーバーへの不要なリクエストを避けるためにsymfonyはこれらのふるまいをオーバーライドできます。

通常は、sfResponseオブジェクトのメソッドを呼び出すことでこのふるまいを起動させます。アクションにおいて、ページがキャッシュされる最大時間(秒)を定義します:

[php]
$this->getResponse()->addCacheControlHttpHeader('max_age=60');

ページがキャッシュされる条件も指定できるので、プロバイダのキャッシュは(銀行口座の番号のように)秘密のデータのコピーを保存しません:

[php]
$this->getResponse()->addCacheControlHttpHeader('private=True');

HTTPのCache-Controlヘッダーを使うことで、サーバーとクライアントブラウザー間でのさまざまなキャッシュメカニズムを調整する機能が得られます。これらのディレクティブの詳細な概説については、W3CのCache-Controlの仕様をご覧ください。

最後の1つのExpiresヘッダーはsymfonyを通して設定できます:

[php]
$this->getResponse()->setHttpHeader('Expires', $this->getResponse()->getDate($timestamp));

CAUTION Cache-Controlのメカニズムを有効にした場合、主な結果はサーバーのログがユーザーによって発せられたすべてのリクエストではなく、実際に受けとったリクエストのみを表示するようになることです。パフォーマンスがよくなれば、統計データではサイトの見た目の人気は減ることがあります。

まとめ

選択したキャッシュタイプにしたがって、キャッシュシステムは可変のパフォーマンスの加速を提供します。ゲインが最大から最小のものから、キャッシュタイプはつぎのものがあります。

加えて、部分テンプレートとコンポーネントも同様にキャッシュされます。

モデルもしくはセッション内のデータを変更することで一貫性のためにキャッシュを削除することが強制される場合、あなたはきめ細かい粒度で行うことができます。変更された要素のみが削除され、そのほかのものは保たれます。

あなたが間違った要素をキャッシュする場合、もしくは内在するデータを更新したときにキャッシュをクリアすることを忘れた場合、新しいバグが現れるかもしれないので、 キャッシュが有効にされたすべてのページを注意深くテストすることを忘れないでください。ステージング(staging)環境は、キャッシュのテスト専用で、そのために大いに役立ちます。

最後に、HTTP 1.1のプロトコルをsymfonyの高度なキャッシュ調整機能のなかで最大限活用してください。このことによってキャッシュタスクのなかでクライアントに影響を与えさらなるパフォーマンスのゲインを提供することになります。