今日は、キャッシュの話をします。symfonyフレームワークには、多くのキャッシュ機構が組み込まれています。 たとえば、YAML設定ファイルは最初にPHPに変換され、ファイルシステムにキャッシュされます。 adminジェネレーターによって生成されたモジュールが、パフォーマンスを向上させるためにキャッシュされることもすでに見てきました。
しかし今日は、別のキャッシュ: HTMLキャッシュについて話します。 Webサイトのパフォーマンスを改善するために、HTMLページ全体もしくは一部だけをキャッシュできます。
新しい環境を作成する
symfonyのテンプレートキャッシュ機能は、デフォルトのsettings.yml
設定ファイルのprod
環境では有効ですが、test
やdev
環境では有効になっていません:
prod: .settings: cache: on dev: .settings: cache: off test: .settings: cache: off
運用に移行する前にキャッシュ機能をテストする必要があるので、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://jobeet.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: on cache: on etag: off
この設定では、cache
設定でsymfonyのテンプレートキャッシュ機能を有効にし、web_debug
設定でWebデバッグツールバーを有効にしています。
デフォルトのコンフィギュレーションでは、すべての設定をキャッシュするので、キャッシュをクリアするまで設定の変更が有効になりません:
$ php symfony cc
ブラウザーでページを更新すると、dev
環境と同じようにWebデバッグツールバーがページ右上に表示されます。
キャッシュのコンフィギュレーション
symfonyのテンプレートキャッシュはcache.yml
設定ファイルで設定できます。
アプリケーションごとのデフォルトコンフィギュレーションはapps/frontend/config/cache.yml
にあります:
default: enabled: off with_layout: false lifetime: 86400
symfonyで扱うすべてのページは動的な情報を持つことができるので、デフォルトでは、グローバルなキャッシュは無効に設定されています(enabled: off
)。
ページごとにキャッシュを有効にする予定なので、今回はグローバルな設定を変更する必要はありません。
lifetime
設定には、サーバーサイドのキャッシュの有効期間を秒単位で定義します(86400
秒は1日に等しい)。
tip
次善策があります: グローバルなキャッシュを有効にして、キャッシュさせたくない特定のページでのみキャッシュを無効にします。 開発するアプリケーションで、作業が少なくて済む方法を選択してください。
ページのキャッシュ
JobeetのホームページはWebサイトの中でおそらくもっとも訪問されるページになりますので、ユーザーがアクセスするたびにデータベースにデータをリクエストする代わりにページをキャッシュしましょう。
sfJobeetJob
モジュール用のcache.yml
ファイルを作ります:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml index: enabled: on 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: on index: enabled: on 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: on index: enabled: on all: with_layout: false
with_layout
設定をfalse
に変更することで、レイアウトのキャッシュを無効にしました。
キャッシュをクリアします:
$ php symfony cc
違いを確認するためにブラウザーのページを更新します:
簡略化されたダイアグラムではリクエストのフローがレイアウトありの場合とよく似ていますが、レイアウトなしのキャッシュでは、はるかにリソースを集約します。
パーシャルとコンポーネントのキャッシュ
高度に動的なサイトでは、アクションテンプレート全体をキャッシュできない場合があります。 これらのケースでは、キャッシュをきめ細かく設定する方法が必要になります。 ありがたいことに、パーシャルとコンポーネントもキャッシュできます。
language
コンポーネントをキャッシュできるように、sfJobeetLanguage
モジュール用のcache.yml
ファイルを作りましょう:
# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml _language: enabled: on
パーシャルもしくはコンポーネントに対してキャッシュを設定するには、設定ファイルに名前つきのエントリーを追加します。
この種類のキャッシュでは、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 __construct(sfDoctrineRecord $object = null, $options = array(), $CSRFSecret = null) { parent::__construct($object, $options, false); } // ... }
この変更の後で、期待通りに動作するか検証するために、キャッシュをクリアして上記のシナリオを繰り返してください。
レイアウトには言語フォームも含まれておりキャッシュに保存されるので、言語フォームにも同じコンフィギュレーションを適用する必要があります。
デフォルトの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'))); unset($this->form[$this->form->getCSRFFieldName()]); } } // 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'))); unset($form[$form->getCSRFFieldName()]); // ... } }
getCSRFFieldName()
メソッドはCSRFトークンを含むフィールドの名前を返します。
このフィールドをunset
すると、ウィジェットと関連するバリデーターは削除されます。
キャッシュを削除する
ユーザーが求人を投稿してアクティベートするたびに、新しい求人を一覧に表示するためにホームページをリフレッシュしなければなりません。
リアルタイムで求人をホームページに表示させる必要はないので、ベストな戦略はキャッシュの有効期間を短くすることです:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml index: enabled: on 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: on web_debug: off etag: off
求人作成ページをテストしましょう:
// test/functional/frontend/jobActionsTest.php $browser-> info(' 7 - Job creation page')-> get('/fr/')-> with('view_cache')->isCached(true, false)-> createJob(array('category_id' => Doctrine::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つのbooleanを受け取ります:
- ページがキャッシュされているかどうか
- レイアウト付きのキャッシュかどうか
tip
機能テストフレームワークによって提供されるすべてのツールがあるにせよ、ブラウザーで問題を診断するほうが簡単であることがあります。
これを実行するのは簡単でtest
環境用のフロントコントローラーを作るだけです。
log/frontend_test.log
に保存されるログも非常に役立ちます。
また明日
他の多くのsymfonyの機能のように、symfonyのキャッシュサブフレームワークはとても柔軟なので、開発者はキャッシュをきめ細かく設定できます。
明日は、アプリケーションのライフサイクルの一番最後のステップ: 運用サーバーへのデプロイについて話します。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.