今日は、キャッシュの話をします。symfony フレームワークには、多くのキャッシュ機構が組み込まれています。たとえば、YAML 設定ファイルは最初に PHP に変換され、ファイルシステムにキャッシュされます。アドミンジェネレータによって生成されたモジュールが、パフォーマンスを向上させるためにキャッシュされることもすでに見てきました。
しかし今日は、別のキャッシュ: HTML キャッシュについて話します。Web サイトのパフォーマンスを改善するために、HTML ページ全体もしくは一部だけをキャッシュできます。
新しい環境を作成する
symfony のテンプレートキャッシュ機能は、デフォルトの settings.yml
設定ファイルの prod
環境では有効ですが、test
や dev
環境では有効になっていません:
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
環境用のフロントコントローラを作ります:
// 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
環境固有のコンフィギュレーションを追加します:
# 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 デバッグツールバーを有効にしています。
デフォルトのコンフィギュレーションでは、すべての設定をキャッシュするので、キャッシュをクリアするまで設定の変更が有効になりません:
$ php symfony cc
ブラウザでページを更新すると、dev
環境と同じように Web デバッグツールバーがページ右上に表示されます。
キャッシュのコンフィギュレーション
symfony のテンプレートキャッシュは cache.yml
設定ファイルで設定できます。アプリケーションごとのデフォルトコンフィギュレーションは apps/frontend/config/cache.yml
にあります:
default: enabled: false with_layout: false lifetime: 86400
symfony で扱うすべてのページは動的な情報をもつことができるので、デフォルトでは、グローバルなキャッシュは無効に設定されています (enabled: false
)。ページごとにキャッシュを有効にする予定なので、今回はグローバルな設定を変更する必要はありません。
lifetime
設定には、サーバーサイドのキャッシュの有効期間を秒単位で定義します (86400
秒は1日に等しい)。
tip
次善策があります: グローバルなキャッシュを有効にして、キャッシュさせたくない特定のページでのみキャッシュを無効にします。開発するアプリケーションで、作業が少なくて済む方法を選択してください。
ページのキャッシュ
Jobeet のホームページは Web サイトのなかでおそらくもっとも訪問されるページになりますので、ユーザーがアクセスするたびにデータベースにデータをリクエストする代わりにページをキャッシュしましょう。
sfJobeetJob
モジュールの cache.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 ではキャッシュされません。
求人作成ページもキャッシュできます:
# 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
モジュールのキャッシュコンフィギュレーションを次のように変更します:
# 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
ファイルを作りましょう:
# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml _language: enabled: true
パーシャルもしくはコンポーネントに対してキャッシュを設定するには、設定ファイルに名前つきのエントリを追加します。この種類のキャッシュでは、with_layout
オプションは意味がないので指定しても無視されます:
フォームにおけるキャッシュ
求人作成ページにはフォームが含まれるので、キャッシュに保存することには問題があります。問題をよりわかりやすくするために、ブラウザで「Post a Job」ページに移動してキャッシュを生成させます。それから、セッション Cookie をクリアし、求人の投稿を試します。「CSRF 攻撃」を警告するエラーメッセージが表示されます:
なぜでしょうか?フロントエンドアプリケーションを作成したとき CSRF 用の秘密の文字列を設定したので、symfony はすべてのフォームに CSRF トークンを埋め込みます。CSRF 攻撃を防ぐために、このトークンはユーザーとフォームに対してユニークです。
最初にページが表示されるときに、生成された HTML フォームは現在のユーザーのトークンとともにキャッシュに保存されます。次に別のユーザーがフォームにアクセスすると、キャッシュからのページは最初のユーザーの CSRF トークンのまま表示されます。フォームを投稿すると、トークンが一致せず、エラーが表示されます。
フォームをキャッシュに保存するのは適切だと思われますが、この問題を修正するにはどうしたらよいでしょうか?求人作成フォームはユーザーに依存せず、現在のユーザーに対して何も変更しません。このような場合、CSRF の防御が不要なので、CSRF トークンを完全に削除できます:
// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.php abstract PluginJobeetJobForm extends BaseJobeetJobForm { public function configure() { $this->disableLocalCSRFProtection(); } }
この変更の後で、期待どおりに動作するか検証するために、キャッシュをクリアして上記のシナリオを繰り返してください。
レイアウトには言語フォームも含まれておりキャッシュに保存されるので、言語フォームにも同じコンフィギュレーションを適用する必要があります。デフォルトの sfLanguageForm
を使い、新しいクラスを作る代わりに CSRF トークンを削除します。sfJobeetLanguage
モジュールのアクションとコンポーネントで次のように変更します:
// 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
すると、ウィジェットと関連するバリデータは削除されます。
キャッシュを削除する
ユーザーが求人を投稿してアクティベートするたびに、新しい求人を一覧に表示するためにホームページをリフレッシュしなければなりません。
リアルタイムで求人をホームページに表示させる必要はないので、ベストな戦略はキャッシュの有効期間を短くすることです:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml index: enabled: true lifetime: 600
デフォルトの設定の1日ではなく10分ごとに、ホームページのキャッシュは自動的に削除されます。
ユーザーが新しい求人をアクティベートしたらすぐにホームページを更新したい場合は、sfJobeetJob
モジュールの executePublish()
メソッドを編集してキャッシュをクリアする機能を追加します:
// 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
環境のコンフィギュレーションを変更します:
# 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
求人作成ページをテストしましょう:
// test/functional/frontend/jobActionsTest.php $browser-> info(' 7 - Job creation page')-> get('/fr/')-> with('view_cache')->isCached(true, false)-> createJob(array('category_id' => Doctrine_Core::getTable('CategoryTranslation')->findOneBySlug('programming')->getId()), true)-> 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 のキャッシュサブフレームワークはとても柔軟なので、開発者はキャッシュをきめ細かく設定できます。
明日は、アプリケーションのライフサイクルの一番最後のステップ: 運用サーバーへのデプロイについて話します。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.