21日目: キャッシュ
今日は、~キャッシュ~の話をします。symfony フレームワークには、多くのキャッシュ機構が組み込まれています。たとえば、~YAML~~ 設定|コンフィギュレーション~ファイルは最初に PHP に変換され、ファイルシステムにキャッシュされます。アドミンジェネレータによって生成されたモジュールが、~パフォーマンス~を向上させるためにキャッシュされることもすでに見てきました。
しかし今日は、別のキャッシュ: HTML キャッシュについて話します。Web サイトのパフォーマンスを改善するために、HTML ページ全体もしくは一部だけをキャッシュできます。
新しい環境を作成する
symfony の~テンプレートキャッシュ|テンプレート (キャッシュ)~機能は、デフォルトの settings.yml
設定ファイルの prod
~環境~では有効ですが、test
や dev
環境では有効になっていません:
[yml]
prod:
.settings:
cache: true
dev:
.settings:
cache: false
test:
.settings:
cache: false
運用に移行する前にキャッシュ機能をテストする必要があるので、dev
環境用のキャッシュを有効にするか新しい環境を作ります。環境は名前 (文字列)、関連するフロントコントローラ、オプションとして固有の設定値のセットによって定義されることを思い出しましょう。
Jobeet でキャッシュシステムを試すために、cache
環境を作ります。cache
環境は prod
環境と似ていますが、dev
環境で利用可能なログとデバッグ情報も有効にします。
dev
環境のフロントコントローラである web/frontend_dev.php
を web/frontend_cache.php
にコピーして、新しい cache
環境用のフロントコントローラを作ります:
[php]
// web/frontend_cache.php
if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1')))
{
die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'cache', true);
sfContext::createInstance($configuration)->dispatch();
作業はこれだけです。これで新しい cache
環境を利用できます。唯一の違いは、getApplicationConfiguration()
メソッドの第2引数で環境の名前が cache
になっていることです。
ブラウザでこのフロントコントローラを呼び出すことで cache
環境をテストできます:
http://www.jobeet.com.localhost/frontend_cache.php/
NOTE フロントコントローラスクリプトの先頭には、ローカルの IP アドレスからのみ呼び出されることを保証するコードがあります。このセキュリティ対策は運用サーバーでフロントコントローラが呼び出されないようにするためです。明日のチュートリアルでより詳しく話します。
現在は、cache
環境はデフォルトのコンフィギュレーションを継承しています。settings.yml
設定ファイルを編集して、cache
環境固有のコンフィギュレーションを追加します:
[yml]
# apps/frontend/config/settings.yml
cache:
.settings:
error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?>
web_debug: true
cache: true
etag: false
この設定では、cache
設定で symfony のテンプレートキャッシュ機能を有効にし、web_debug
設定で ~Web デバッグツールバー~を有効にしています。
databases.yml
を編集してファイルの始めに次の設定を追加します:
[yml]
# config/databases.yml
cache:
propel:
class: sfPropelDatabase
param:
classname: DebugPDO
デフォルトのコンフィギュレーションでは、すべての設定をキャッシュするので、キャッシュをクリアするまで設定の変更が有効になりません:
$ php symfony cc
ブラウザでページを更新すると、dev
環境と同じように Web デバッグツールバーがページ右上に表示されます。
キャッシュのコンフィギュレーション
symfony のテンプレートキャッシュは ~cache.yml
~ 設定ファイルで設定できます。アプリケーションごとのデフォルトコンフィギュレーションは apps/frontend/config/cache.yml
にあります:
[yml]
default:
enabled: false
with_layout: false
lifetime: 86400
symfony で扱うすべてのページは動的な情報をもつことができるので、デフォルトでは、グローバルなキャッシュは無効に設定されています (enabled: false
)。ページごとにキャッシュを有効にする予定なので、今回はグローバルな設定を変更する必要はありません。
lifetime
設定には、サーバーサイドの~キャッシュの有効期間~を秒単位で定義します (86400
秒は1日に等しい)。
TIP 次善策があります: グローバルなキャッシュを有効にして、キャッシュさせたくない特定のページでのみキャッシュを無効にします。開発するアプリケーションで、作業が少なくて済む方法を選択してください。
ページのキャッシュ
Jobeet のホームページは Web サイトのなかでおそらくもっとも訪問されるページになりますので、ユーザーがアクセスするたびにデータベースにデータをリクエストする代わりにページをキャッシュしましょう。
sfJobeetJob
モジュールの cache.yml
ファイルを作ります:
[yml]
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
index:
enabled: true
with_layout: true
TIP
cache.yml
設定ファイルには、view.yml
と同じような symfony の設定ファイルのプロパティがあります。たとえば、特別なall
キーを使うと、モジュールのすべてのアクションに対してキャッシュを有効にできます。
ブラウザでページを更新すると、コンテンツがキャッシュされたことを示すボックスが表示されているのがわかります:
ボックスには、キャッシュの有効期間や経過時間など、キャッシュキーに関するデバッグのための貴重な情報が表示されます。
ページを再度更新すると、ボックスの色が緑から黄色に変わります。これはページがキャッシュから読み込まれたことを示します:
2番目のケースでは、Web デバッグツールバーで示されるように、データベースへのリクエストが行われなかったことがわかります。
TIP 言語をユーザーごとに変更できる場合でも、言語を URL に埋め込むようにすればキャッシュは機能します。
ページがキャッシュ可能で、キャッシュがまだ存在しない場合、symfony はリクエスト処理の最後でレスポンスオブジェクトをキャッシュに保存します。これ以降のすべてのリクエストでは、symfony はコントローラを呼び出さずにキャッシュされたレスポンスを送信します:
JMeterのようなツールを利用することで、~パフォーマンス~が大きく変化することを自分自身で測定できます。
NOTE パラメータを伴う
GET
リクエストや、POST
、PUT
、DELETE
メソッドで投稿されるリクエストは、コンフィギュレーションにかかわらず、symfony ではキャッシュされません。
求人作成ページもキャッシュできます:
[yml]
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
new:
enabled: true
index:
enabled: true
all:
with_layout: true
2つのページともにレイアウト付きでキャッシュできるので、sfJobeetJob
モジュールのすべてのアクション用のデフォルトコンフィギュレーションを定義する all
セクションを作りました。
キャッシュをクリアする
ページのキャッシュをクリアしたい場合、cache:clear
タスクを使います:
$ php symfony cc
cache:clear
タスクでは、メインの cache/
ディレクトリに保存されたすべてのキャッシュがクリアされます。このタスクには、キャッシュの一部を指定してクリアするためのオプションもあります。cache
環境のテンプレートキャッシュのみをクリアするには、--type
と --env
オプションを指定します:
$ php symfony cc --type=template --env=cache
変更を行うたびにキャッシュをクリアする代わりに、クエリ文字列を URL に追加するか、Web デバッグツールバーから「Ignore cache」ボタンを使うことでキャッシュを無効にできます:
アクションキャッシュ
ページ全体のキャッシュはできなくても、~アクション~テンプレートのキャッシュが可能な場合もあります。それでは、レイアウト以外のすべてをキャッシュする例をみてみましょう。
Jobeet アプリケーションでは、「history job」バーがあるためにページ全体をキャッシュできません。
job
モジュールのキャッシュコンフィギュレーションを次のように変更します:
[yml]
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
new:
enabled: true
index:
enabled: true
all:
with_layout: false
with_layout
設定 を false
に変更することで、レイアウトのキャッシュを無効にしました。
キャッシュをクリアします:
$ php symfony cc
違いを確認するためにブラウザのページを更新します:
簡略化されたダイアグラムではリクエストのフローがレイアウトありの場合とよく似ていますが、レイアウトなしのキャッシュでは、はるかにリソースを集約します。
~パーシャル|パーシャルテンプレート~と~コンポーネント~のキャッシュ
高度に動的なサイトでは、アクションテンプレート全体をキャッシュできない場合があります。これらのケースでは、キャッシュをきめ細かく設定する方法が必要になります。ありがたいことに、パーシャルとコンポーネントもキャッシュできます。
language
コンポーネントをキャッシュできるように、sfJobeetLanguage
モジュールの cache.yml
ファイルを作りましょう:
[yml]
# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml
_language:
enabled: true
パーシャルもしくはコンポーネントに対してキャッシュを設定するには、設定ファイルに名前つきのエントリを追加します。この種類のキャッシュでは、with_layout
オプションは意味がないので指定しても無視されます:
SIDEBAR ~コンテキスト依存|コンテキスト上のキャッシュ~であるか?
多くの異なるテンプレートから、同じコンポーネントもしくはパーシャルを使うことができます。たとえば job の
_list.php
パーシャルはsfJobeetJob
とsfJobeetCategory
モジュールで使われます。レンダリングは常に同じなので、パーシャルは使われるコンテキストに依存せず、キャッシュはすべてのテンプレートに対して同じです (キャッシュは異なるパラメータに対して明らかに異なります)。しかし、パーシャルもしくはコンポーネントの出力は、含まれるアクションに基づいて異なる場合があります。たとえばブログのサイドバーは、ホームページとブログ投稿ページでは微妙に違います。このような場合、パーシャルもしくはコンポーネントはコンテキスト依存なので、キャッシュの設定で
contextual
オプションをtrue
にセットする必要があります:[yml] _sidebar: enabled: true contextual: true
フォームにおけるキャッシュ
求人作成ページにはフォームが含まれるので、キャッシュに保存することには問題があります。問題をよりわかりやすくするために、ブラウザで「Post a Job」ページに移動してキャッシュを生成させます。それから、セッション Cookie をクリアし、求人の投稿を試します。「~CSRF~ 攻撃」を警告するエラーメッセージが表示されます:
なぜでしょうか?フロントエンドアプリケーションを作成したとき CSRF 用の秘密の文字列を設定したので、symfony はすべてのフォームに CSRF トークンを埋め込みます。CSRF 攻撃を防ぐために、このトークンはユーザーとフォームに対してユニークです。
最初にページが表示されるときに、生成された HTML フォームは現在のユーザーのトークンとともにキャッシュに保存されます。次に別のユーザーがフォームにアクセスすると、キャッシュからのページは最初のユーザーの CSRF トークンのまま表示されます。フォームを投稿すると、トークンが一致せず、エラーが表示されます。
フォームをキャッシュに保存するのは適切だと思われますが、この問題を修正するにはどうしたらよいでしょうか?求人作成フォームはユーザーに依存せず、現在のユーザーに対して何も変更しません。このような場合、CSRF の防御が不要なので、CSRF トークンを完全に削除できます:
[php]
この変更の後で、期待どおりに動作するか検証するために、キャッシュをクリアして上記のシナリオを繰り返してください。
レイアウトには言語フォームも含まれておりキャッシュに保存されるので、言語フォームにも同じコンフィギュレーションを適用する必要があります。デフォルトの sfLanguageForm
を使い、新しいクラスを作る代わりに CSRF トークンを削除します。sfJobeetLanguage
モジュールのアクションとコンポーネントで次のように変更します:
[php]
// plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/components.class.php
class sfJobeetLanguageComponents extends sfComponents
{
public function executeLanguage(sfWebRequest $request)
{
$this->form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
$this->form->disableLocalCSRFProtection();
}
}
// plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/actions.class.php
class sfJobeetLanguageActions extends sfActions
{
public function executeChangeLanguage(sfWebRequest $request)
{
$form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
$form->disableLocalCSRFProtection();
// ...
}
}
getCSRFFieldName()
メソッドは CSRF トークンを含むフィールドの名前を返します。このフィールドを unset
すると、ウィジェットと関連するバリデータは削除されます。
~キャッシュを削除する|キャッシュの削除~
ユーザーが求人を投稿してアクティベートするたびに、新しい求人を一覧に表示するためにホームページをリフレッシュしなければなりません。
リアルタイムで求人をホームページに表示させる必要はないので、ベストな戦略はキャッシュの有効期間を短くすることです:
[yml]
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml
index:
enabled: true
lifetime: 600
デフォルトの設定の1日ではなく10分ごとに、ホームページのキャッシュは自動的に削除されます。
ユーザーが新しい求人をアクティベートしたらすぐにホームページを更新したい場合は、sfJobeetJob
モジュールの executePublish()
メソッドを編集してキャッシュをクリアする機能を追加します:
[php]
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
$request->checkCSRFProtection();
$job = $this->getRoute()->getObject();
$job->publish();
if ($cache = $this->getContext()->getViewCacheManager())
{
$cache->remove('sfJobeetJob/index?sf_culture=*');
$cache->remove('sfJobeetCategory/show?id='.$job->getJobeetCategory()->getId());
}
$this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));
$this->redirect($this->generateUrl('job_show_user', $job));
}
キャッシュは sfViewCacheManager
クラスによって管理されます。remove()
メソッドは、引数で指定した内部 URI に関連するキャッシュを削除します。変数の可能なすべてのパラメータに対するキャッシュを削除するには、*
を値として使います。上記のコードで使った sf_culture=*
は、symfony が英語とフランス語のホームページのキャッシュを削除することを意味します。
キャッシュが無効なときキャッシュマネージャは null
なので、if
ブロックでキャッシュの削除処理を囲みました。
~キャッシュをテストする|テスト (キャッシュ)~
テストを始める前に、キャッシュレイヤーを有効にするために test
環境のコンフィギュレーションを変更します:
[yml]
# apps/frontend/config/settings.yml
test:
.settings:
error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
cache: true
web_debug: false
etag: false
求人作成ページをテストしましょう:
[php]
// test/functional/frontend/jobActionsTest.php
$browser->
info(' 7 - Job creation page')->
get('/fr/')->
with('view_cache')->isCached(true, false)->
get('/fr/')->
with('view_cache')->isCached(true, false)->
with('response')->checkElement('.category_programming .more_jobs', '/23/')
;
view_cache
テスターはキャッシュをテストするために使います。isCached()
メソッドは2つのブール値を受け取ります:
- ページがキャッシュされているかどうか
- レイアウトつきのキャッシュかどうか
TIP 機能テストフレームワークによって提供されるすべてのツールがあるにせよ、ブラウザで問題を診断するほうが簡単であることがあります。これを実行するのは簡単で
test
環境のフロントコントローラを作るだけです。log/frontend_test.log
に保存される~ログ|ロギング~も非常に役立ちます。
また明日
ほかの多くの symfony の機能のように、symfony のキャッシュサブフレームワークはとても柔軟なので、開発者はキャッシュをきめ細かく設定できます。
明日は、アプリケーションのライフサイクルの一番最後のステップ: 運用サーバーへのデプロイについて話します。
ORM