第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: true

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

静的な情報 (データベースに依存しないもしくはセッションに依存するデータ) を表示するアクション、もしくは修正なしでデータベースから情報を読みとるアクション (典型的なものは 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 - 全内容の例 (frontend/modules/user/config/cache.yml)

list:
  enabled:    true
show:
  enabled:    true

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:    true
list:
  enabled:    true
...

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

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

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

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

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

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

_day:
  enabled: true

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

_day:
  contextual: true
  enabled:    true

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; ?>

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

  • 'users' と名づけられたキャッシュ済みのフラグメントが見つかる場合、これは <?php if (!cache($unique_fragment_name)): ?><?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で示されるように、アプリケーションの lib/ ディレクトリのなかで conditionalCacheFilter を作成します。

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

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

...
security: ~

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

cache: ~
...

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

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つ (sfAPCCachesfEAcceleratorCachesfMemcacheCachesfSQLiteCachesfXCacheCache を含む) に置き換えることができます。param キーの下で定義されたパラメーターは連想配列としてカスタムクラスの initialize() メソッドに渡されます。どのビューキャッシュストレージクラスも sfCache抽象クラスのメソッドをすべて実装しなければなりません。この主題に関する詳しい情報は19章を参照してください。

memcacheサーバーを2つ使用する場合の設定 viewcache: class: sfMemcacheCache param: servers: server1: host: 192.168.1.10 server_2: host: 192.168.1.11

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

キャッシュされたページでさえいくつかの 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 スクリプトをキャッシュすることでパフォーマンスを増大させるので、コードの解析とコンパイルのオーバーヘッドは徹底的に削減されます。これは大量のコードを含む ORM のクラスに対してとりわけ効果的です。これらのアクセラレータは symfony と互換性があり、簡単にアプリケーションの速度を3倍にします。これらは運用環境で大規模なユーザーをかかえる symfony のアプリケーションに推奨されます。

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

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

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

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

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

リスト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

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

データベースが更新されたとき、修正されたデータに関連するアクションのキャッシュをクリアしなければなりません。キャッシュ全体をクリアできましたが、モデルの変更に関係のない既存のすべてのキャッシュされたアクションに対しては無駄な作業です。これが 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);
  ...
}

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

[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() メソッド呼び出しを格納すべきであることを覚えておいてください。

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

一度に複数のキャッシュ部分をクリアする

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 だけでなく、sfAPCCachesfEAcceleratorCachesfMemcacheCachesfSQLiteCachesfXCacheCache も)。

複数のアプリケーションにまたがるキャッシュをクリアする

複数のアプリケーションにまたがるキャッシュをクリアすることが問題になる可能性があります。たとえば、管理者が 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_cache_dir').DIRECTORY_SEPARATOR.'frontend'.
 ➥ DIRECTORY_SEPARATOR.sfConfig::get('sf_environment').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 設定は true です。

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

リスト12-12 - staging 環境のための設定 (frontend/config/settings.yml)

staging:
  .settings:
    web_debug:  true
    cache:      true

加えて、運用環境のフロントコントローラー (おそらく 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 環境でブラウジングしているとき、表示される処理時間はデバッグモードが無効になっている運用環境では何が起きるのかを示しません。

それぞれのリクエストの処理時間の詳細な概要を知るには、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 を理解しない場合でも、それを利用するリクエストのルート内部でプロキシが存在できます。

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

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

ETag: "1A2Z3E4R5T6Y7U"

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

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

all:
  .settings:
    etag: true

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

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

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

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

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

If-Modified-Since: Sat, 23 Nov 2010   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 の高度なキャッシュ調整機能のなかで最大限活用してください。このことによってキャッシュタスクのなかでクライアントに影響を与えさらなるパフォーマンスのゲインを提供することになります。