CakePHP でビューキャッシュを使用時、独自Viewクラスを引き継ぐ(不完全)
概要
ビューキャッシュは、コアの CacheHelper によって作成され、CacheDispatcher で読み込みが行われます。
但し、現時点では、viewClass を引き継ぐ方法がないため、View クラスによるレンダリングが行われます。
通常は、View クラスのレンダリングで問題はないのですが、『素のPHPまたはCakePHPの テンプレートのHTML部分を圧縮(minify) して通信量を削減により、高速化を図る』で、nocacheコメントで囲まれた部分で、動的にエレメントの表示を制御している場合に問題があります。テンプレートファイルのHTML圧縮時に呼ばれないエレメントファイルは、圧縮されず、ビューキャッシュからのレンダリングからされる場合は、View クラスが使用されるため、HTML部分を圧縮する機会を失ってしまいます。
解決策として、
- デプロイ時に、テンプレートのHTML圧縮を行う
- ビューキャッシュを作成する際、独自Viewクラスを引き継がせる
があります。
事前圧縮だけでなく、オンデマンドでのテンプレート圧縮も提供したいので、ビューキャッシュを作成する際、独自Viewクラスを引き継がせる方法を考えました。
但し、
今回考えた方法は、適応できる状況に制限があり、問題なく独自Viewクラスを引き継ぐには CakePHP コアによるサポートが必須になります。
環境
- CakePHP 2.3.10
CakePHP 2.4 や CakePHP 3 でも CacheHelper や CacheDispatcher は変化していません。
処理の流れ
まずは、CacheDispatcher の処理の流れをおさらいします。
CacheDispatcher の処理の流れ
- beforeDispatch イベントで発火
Configure::read('Cache.check')
が有効かチェックCakeRequest::here
からキャッシュファイルパスを生成- キャッシュファイルが存在するかチェック
- View クラス(決め打ち)のインスタンスを生成
View::renderCache
にキャッシュファイルパスを渡す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 のように書き出します。
<!--cachetime:1388384352--><!--viewClass:MyView--><!--location:MyPlugin.View--><?php
CacheDispatcher を継承したクラスで、
- キャッシュファイルの存在をチェック
- file_get_contents で読み込み(この時点ではPHPとして評価していない)
- 正規表現で、cachetime、viewClass、location を抜き出す
- 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']) { //@codingStandardsIgnoreStart @unlink($filename); //@codingStandardsIgnoreEnd 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
のような継承元を再定義する構造がない
- ビューのクラス には、