symfonyのキャッシュの挙動まとめ
symfonyのキャッシュについて、設定値の組み合わせによって変わる挙動を調べてみました。
はじめに・キャッシュのいろいろ
symfonyで扱うキャッシュにはいくつかの種類があります。
- アクションキャッシュ
- サーバー側。アクションの実行結果がキャッシュされます。
- ページキャッシュ
- サーバー側。特定のリクエストの実行結果のページ全体がキャッシュされます。
- クライアントキャッシュ
- クライアント側。キャッシュ制御ヘッダなどにより、ブラウザでキャッシュされます。
settings.yml→cacheのon/off
symfonyのキャッシュ機能を有効にするには、settings.ymlでcache = on にします。
prod: .settings: cache: on
このフラグのon/offの違いは以下の通りです。
cache=off
デフォルトのfilterチェインにsfCacheFilterが追加されません。
また、グローバルな設定値「sf_cache」がfalseとなります。
cache=on
デフォルトのfilterチェインにsfCacheFilterが追加されます。
sf_cacheがtrueになります。
sfCacheFilterが追加されるかどうかは、symfonyライブラリの次のファイルの %SF_CACHE% 部分で制御されています。
# /lib/config/config/filters.yml cache: class: sfCacheFilter param: condition: %SF_CACHE%
このファイルのパースがsfFilterConfigHandlerで行われ、conditionの値に応じてフィルタエントリが設定に追加されるかどうかを制御しています。
(プロジェクトのキャッシュディレクトリ内の、config/modules_(モジュール名)_config_filters.yml.phpに出力されます)
cache.yml→enabledのon/off、with_layout、lifetime、client_lifetime
settings.ymlでcache=onに設定しただけでは、実際にキャッシュの機能は動作しません。
キャッシュの機能を実行するには、cache.ymlにてenabled=onに設定して初めて、キャッシュが実際に動作します。
この先は、設定に応じて使われる仕組みの組み合わせが変わってきます。
enabled=on / with_layout=false / lifetime > 0
enabledフラグだけを有効にすると、「アクションキャッシュ」の機能が有効になります。アクションの実行結果がサーバー側でキャッシュされます。クライアント(ブラウザ)側では、キャッシュは行われません。
※lifetime=0にすると、アクションはキャッシュされません。
実際、sfExecutionFilterからviewのレンダリング(render)が実行された時点で、実行結果がキャッシュに保存されます。PHPViewの場合はsfPHPView::rener()内。
キャッシュが保存されているアクションは、次回実行時にsfExecutionFilterのexecuteAction()の呼び出しが省略され、レンダリング時点(sfPHPView::render)で対応するキャッシュから結果が読み込まれます。
enabled=on / with_layout=true / lifetime > 0
enabledフラグと合わせてwith_layoutをtrueにすると、「アクションキャッシュ」ではなく「ページキャッシュ」の機能が有効になります。(lifetimeが0より大きい場合)
この場合は、sfCacheFilter::executeBeforeRendering内で、特定のURLに対するレンダリング結果がキャッシュとして保存されます。(アクションごとのキャッシュは保存されません)
キャッシュが保存されているURLの次回呼び出し時は、sfCacheFilter::executeBeforeExecutionでキャッシュから実行結果が復元されます。この場合、アクションの実行はスキップされます。
クライアント側のキャッシュは、この設定では、lifetimeに指定した時間だけキャッシュが有効になるようにヘッダが生成されます(ExpiresヘッダとCache-Controlヘッダのmax-age)
★このExpiresヘッダの日時は、ページキャッシュの作成日時が基準となることに注意が必要です。
enabled=on / with_layout=true / lifetime > 0 / client_lifetime = 0
この場合は、ページキャッシュが有効で、さらに、ブラウザ側には「Expires」ヘッダのみが返されます。(max-ageがつきません)
したがって、次に同じURLをブラウザからリクエストする際は、「If-Modified-Since」ヘッダが付加されます。
If-Modified-Sinceヘッダがあり、URLに対するコンテンツの内容に変化がない場合、symfonyからは「304 Not Modified」というレスポンスが返されるのが望ましい挙動なのですが、おそらく不具合により、304ではなくて通常の200で応答が返ってきます。(後述)
enabled=on / with_layout=true / lifetime > 0 / client_lifetime > 0
client_lifetimeを指定しない場合とほぼ同じですが、Expiresとmax-ageが、client_lifetimeで指定した数字を元に計算されます。(client_lifetimeは、指定しない場合lifetimeの値と同一になります)
settings.yml→etag=on
etagの設定が有効になりますが、cache.ymlで以下の設定の場合にのみ、実際にETagが出力されます。
enabled: on with_layout: true lifetime: 180 # 0よりも大きい値 client_lifetime: 0
つまり、上記の「If-Modified-Since」ヘッダが使われるパターンで、同時に「If-None-Match」ヘッダでETagが指定されるようになります。
※ただし上で述べたように、不具合があるためそれを修正しないとETagが出力されません。
不具合と思われる箇所その1
sfCacheFilter.php executeBeforeExecution()内
http://trac.symfony-project.org/browser/branches/1.2/lib/filter/sfCacheFilter.class.php#L71
<?php 71 public function executeBeforeExecution() 72 { 73 $uri = $this->routing->getCurrentInternalUri(); 74 75 if (is_null($uri)) 76 { 77 return true; 78 } 79 80 // page cache 81 $cacheable = $this->cacheManager->isCacheable($uri); 82 if ($cacheable && $this->cacheManager->withLayout($uri)) 83 { 84 $inCache = $this->cacheManager->getPageCache($uri); $this->response = $this->context->getResponse(); // ←ココ 85 $this->cache[$uri] = $inCache; 86 87 if ($inCache) 88 { 89 // page is in cache, so no need to run execution filter 90 return false; 91 } 92 } 93 94 return true; 95 }
ページキャッシュをキャッシュファイルから復元する処理が84行目のgetPageCache()で、このメソッド内でキャッシュファイルの内容をunserializeし、コンテキストが保持しているsfWebResponseオブジェクトを置き換えます。
しかし、sfCacheFilterなどが保持しているsfWebResponseの参照($this->response)などはsfCacheFilterの初期化(initializeメソッド)時点でコンテキストが保持していたオブジェクトへの参照のままであり、キャッシュから復元したものではありません。
なので、キャッシュから復元した場合はその直後に、sfCacheFilter内で保持している参照を再度更新する必要がある、というのが上記です。
この不具合があるために、sfCacheFilter::checkCacheValidation()内で304ヘッダなどを設定しても、最終的に出力されるレスポンスに反映されない、という挙動になってしまっています。
不具合と思われる箇所その2
sfCacheFilter.php checkCacheValidation内
http://trac.symfony-project.org/browser/branches/1.2/lib/filter/sfCacheFilter.class.php#L209
<?php 207 // conditional GET support 208 // never in debug mode 209 if ($this->response->hasHttpHeader('Last-Modified') && !sfConfig::get('sf_debug')) 210 { 211 $lastModified = $this->response->getHttpHeader('Last-Modified'); if (is_array($lastModified)) { // ←ココ 212 $lastModified = $lastModified[0]; } // ←ココ 213 if ($this->request->getHttpHeader('IF_MODIFIED_SINCE') == $lastModified) 214 { 215 $this->response->setStatusCode(304); 216 $this->response->setHeaderOnly(true); 217 218 if (sfConfig::get('sf_logging_enabled')) 219 { 220 $this->context->getEventDispatcher()->notify(new sfEvent($this, 'application.log', array('Last-Modified matches If-Modified-Since (send 304)'))); 221 } 222 } 223 }
sfWebResponse::getHttpHeader()の戻り値が配列であることを前提としているようですが、そもそもgetHttpHeaderで配列が返ってくることがあるのかちょっと分かりません。通常のヘッダでは文字列が返ってくるようです。
配列が返ってくることも考慮して、上記のように配列の場合は先頭の要素のみを使用するというifを追加します。
この2カ所の修正を行うと、正しく304 Not Modified応答が返されるようになります。