概要
ビューキャッシュは、コアの CacheHelper によって作成され、CacheDispatcher で読み込みが行われます。
但し、現時点では、viewClass を引き継ぐ方法がないため、View クラスによるレンダリングが行われます。
通常は、View クラスのレンダリングで問題はないのですが、『素のPHPまたはCakePHPの テンプレートのHTML部分を圧縮(minify) して通信量を削減により、高速化を図る』で、nocacheコメントで囲まれた部分で、動的にエレメントの表示を制御している場合に問題があります。テンプレートファイルのHTML圧縮時に呼ばれないエレメントファイルは、圧縮されず、ビューキャッシュからのレンダリングからされる場合は、View クラスが使用されるため、HTML部分を圧縮する機会を失ってしまいます。
解決策として、
- デプロイ時に、テンプレートのHTML圧縮を行う
- ビューキャッシュを作成する際、独自Viewクラスを引き継がせる
があります。
事前圧縮だけでなく、オンデマンドでのテンプレート圧縮も提供したいので、ビューキャッシュを作成する際、独自Viewクラスを引き継がせる方法を考えました。
但し、
今回考えた方法は、適応できる状況に制限があり、問題なく独自Viewクラスを引き継ぐには CakePHP コアによるサポートが必須になります。
環境
CakePHP 2.4 や CakePHP 3 でも CacheHelper や CacheDispatcher は変化していません。
処理の流れ
まずは、CacheDispatcher の処理の流れをおさらいします。
CacheDispatcher の処理の流れ
- beforeDispatch イベントで発火
Configure::read('Cache.check')
が有効かチェック
CakeRequest::here
からキャッシュファイルパスを生成
- キャッシュファイルが存在するかチェック
- View クラス(決め打ち)のインスタンスを生成
View::renderCache
にキャッシュファイルパスを渡す
ob_start
で出力のバッファリングを有効にしつつ、include
でキャッシュファイルを読み込み、PHPコードを実行
- バッファリング内容から
cachetime
の部分を正規表現で抜き出し、キャッシュ切れかどうかをチェック
cachetime
以外の部分を返す
View::renderCache
が成功した場合、キャッシュ内容をレスポンスとして返す
改造方針の経緯
- ビュークラスのインスタンス生成
- キャッシュファイルの読み込み
の処理の流れを、
- ビュークラスの特定
- ビュークラスのインスタンス生成
- キャッシュファイルの読み込み
という流れで作成しようとして、キャッシュファイルの生成と同時に、キャッシュファイル名.view.php のようなビュークラスを格納したファイルを生成させました。しかし、キャッシュファイル数が2倍になるので、スマートではないと感じました。
また、キャッシュファイルパスをキーして、1つのファイルに使用するビュークラスの一覧をまとめようとしましたが、キャッシュファイル数が多くなると、オーバーヘッドが無視できなくなりました。
そんな時、
PHPコアから読み解く定石の嘘ホント(Ustream) を見ていて、
2:40以降の説明で、
include, include_once, require ,require_once, eval も同じ一つのオペコードとして定義されています。
とあって、
以下のような処理になりました。
CacheHelper を継承したクラスで、
App::uses($className, $location)
に必要な $className, $location を cachetime のように書き出します。
<?php
CacheDispatcher を継承したクラスで、
- キャッシュファイルの存在をチェック
- file_get_contents で読み込み(この時点ではPHPとして評価していない)
- 正規表現で、cachetime、viewClass、location を抜き出す
- 抜き出し後は、「<!--cachetime ... <?php」までを除外
- eval には最初の「<?php」は不要
- viewClass がデフォルト以外ならば、
App::uses
で読み込み
- viewClass でインスタンスを生成
- 独自Viewクラスに キャッシュファイル名からではなく、文字列からレンダリングを行う
renderCacheString($string, $timeStart)
を定義して、文字列を渡す
ob_start
で出力のバッファリングを有効にしつつ、今度はeval
でキャッシュファイルを読み込み、PHPコードを実行
- キャッシュ内容をレスポンスとして返す
という流れになりました。
実装
<?php
App::uses('CacheHelper', 'View/Helper');
class ZzCacheHelper extends CacheHelper {
protected function _writeFile($content, $timestamp, $useCallbacks = false) {
$now = time();
if (is_numeric($timestamp)) {
$cacheTime = $now + $timestamp;
} else {
$cacheTime = strtotime($timestamp, $now);
}
$path = $this->request->here();
if ($path === '/') {
$path = 'home';
}
$prefix = Configure::read('Cache.viewPrefix');
if ($prefix) {
$path = $prefix . '_' . $path;
}
$cache = strtolower(Inflector::slug($path));
if (empty($cache)) {
return;
}
$cache = $cache . '.php';
$viewClass = get_class($this->_View);
$location = App::location($viewClass);
$file = '<!--cachetime:' . $cacheTime . '--><!--viewClass:' . $viewClass . '--><!--location:' . $location . '--><?php';
if (empty($this->_View->plugin)) {
$file .= "
App::uses('{$this->_View->name}Controller', 'Controller');
";
} else {
$file .= "
App::uses('{$this->_View->plugin}AppController', '{$this->_View->plugin}.Controller');
App::uses('{$this->_View->name}Controller', '{$this->_View->plugin}.Controller');
";
}
$file .= '
$request = unserialize(base64_decode(\'' . base64_encode(serialize($this->request)) . '\'));
$response->type(\'' . $this->_View->response->type() . '\');
$controller = new ' . $this->_View->name . 'Controller($request, $response);
$controller->plugin = $this->plugin = \'' . $this->_View->plugin . '\';
$controller->helpers = $this->helpers = unserialize(base64_decode(\'' . base64_encode(serialize($this->_View->helpers)) . '\'));
$controller->layout = $this->layout = \'' . $this->_View->layout . '\';
$controller->theme = $this->theme = \'' . $this->_View->theme . '\';
$controller->viewVars = unserialize(base64_decode(\'' . base64_encode(serialize($this->_View->viewVars)) . '\'));
Router::setRequestInfo($controller->request);
$this->request = $request;';
if ($useCallbacks) {
$file .= '
$controller->constructClasses();
$controller->startupProcess();';
}
$file .= '
$this->viewVars = $controller->viewVars;
$this->loadHelpers();
extract($this->viewVars, EXTR_SKIP);
?>';
$content = preg_replace("/(<\\?xml)/", "<?php echo '$1'; ?>", $content);
$file .= $content;
return cache('views' . DS . $cache, $file, $timestamp);
}
}
<?php
App::uses('CacheDispatcher', 'Routing/Filter');
class ZzCacheDispatcher extends CacheDispatcher {
public function beforeDispatch(CakeEvent $event) {
if (Configure::read('Cache.check') !== true) {
return;
}
$path = $event->data['request']->here();
if ($path === '/') {
$path = 'home';
}
$prefix = Configure::read('Cache.viewPrefix');
if ($prefix) {
$path = $prefix . '_' . $path;
}
$path = strtolower(Inflector::slug($path));
$filename = CACHE . 'views' . DS . $path . '.php';
if (!file_exists($filename)) {
$filename = CACHE . 'views' . DS . $path . '_index.php';
}
if (!file_exists($filename)) {
return null;
}
$out = file_get_contents($filename);
if (!preg_match('/^<!--cachetime:(\\d+)--><!--viewClass:(.+?)--><!--location:(.+?)--><\?php/', $out, $match)) {
return null;
}
if (time() >= $match['1']) {
@unlink($filename);
unset($out);
return null;
}
$viewClass = $match[2];
if ($viewClass !== 'View') {
App::uses($viewClass, $match[3]);
}
$out = substr($out, strlen($match[0]));
$view = new $viewClass(null);
$view->response = $event->data['response'];
if ($viewClass === 'View' || !method_exists($view, 'renderCacheString')) {
return null;
} else {
$out = $view->renderCacheString($out, microtime(true));
}
$event->stopPropagation();
$event->data['response']->body($out);
return $event->data['response'];
}
}
<?php
App::uses('View', 'View');
class ZzView extends View {
public function renderCacheString($string, $timeStart) {
$response = $this->response;
ob_start();
eval($string);
$type = $response->mapType($response->type());
if (Configure::read('debug') > 0 && $type === 'html') {
echo "<!-- Cached Render Time: " . round(microtime(true) - $timeStart, 4) . "s -->";
}
$out = ob_get_clean();
return $out;
}
}
問題点
この方法ではいくつか問題点があります。
- CacheHelper を継承したクラスで書き出されるキャッシュファイルは、継承元の CacheHelper のと互換性がないため、
View::renderCache
が必ず失敗してしまう
- viewClass と location の埋め込み位置をキャッシュファイルの最後部に配置すれば、互換性を確保できるが、HTML以外の場合、コメント部分が不正な文字列になってしまう。
- JsonView 等の Viewクラスを継承したクラスを使用する場合、用途によっては、独自Viewクラスを継承して再定義したものを使用しないといけない。
- ビューのクラス には、
AppController
のような継承元を再定義する構造がない