原文发布时间:2013年7月4日
CakePHP是一个MVC设计模式下的PHP框架,它使得您的生活更加简单并且让您的开发工作更上一层楼。尽管它被认为是一个相对缓慢的框架,(因为)它带有的大量缓存引擎(例如: FileCache, ApcCache, Wincache, XcacheEngine, MemcacheEngine 以及 RedisEngine等缓存引擎系统)能够帮助您提高您的网页加载或者PHP应用速度。
上面提到的缓存引擎允许您缓存SQL结果集、序列化对象、HTML块元素等等。不幸的是,尽管 CakePHP 2.x 版本支持整页缓存(这可以大幅提高应用程序的速度),但上述引擎并不在内部使用。取而代之的是CakePHP使用缓存助件,它将HTML的源代码直接存储在Web服务器的文件系统上。
这种方法在速度和架构上都存在问题。首先,其他的缓存引擎(例如:ApcCache)速度明显更快,因为它将缓存存储到内存中。同样从架构的角度来看,最好从一个单类来处理缓存。您不想将缓存文件本地存储在您的Web服务器硬盘上的另一个原因是:当您在执行负载均衡操作的时候,即:使用多个Web服务器来托管同一网站的时候。在这种情况下,使用Memcache让您能够从所有的集群服务器中访问缓存页面。即使可以使用例如GlusterFS、CephFS甚至NFS等使用网络连接的存储文件系统,这也会影响基础架构的复杂程度并影响整体速度。
即使Mark Story(一位CakePHP的核心开发人员)已经提出要在2010年纠正这种行为,但迄今为止仍未有人做到这一点。几周前,我碰到了这个它并且我决定去扩展此框架,以便在内部使用缓存引擎进行整页缓存。我将源代码贡献给社区,但不幸的是,它还没有被包含在CakePHP的框架中(可能因为他们计划在下一个版本中改变缓存的工作方式或是因为我没有打算在Git上发送合并请求。无论如何,问题依然存在。)
下面是我发布的扩展此框架的PHP代码。请注意,实际新的代码量不超过15行,但是由于CakePHP的编写方式,需要从框架复制粘贴大量的代码。最后要注意的是,我们不是直接修改框架,而是通过引入3个自定义类来扩展它。
自定义缓存助件强制CakePHP去使用缓存引擎来替代将HTML代码直接写入硬盘是很有必要的:
<?php
/**
* CakePHP补丁:使用缓存引擎扩展CakePHP的缓存助件
* http://www.datumbox.com/
*
* Copyright 2013, Vasilis Vryniotis
* Licensed under MIT or GPLv3, see LICENSE
*/
//在 /app/View/Helper/MyCacheHelper.php 里我们改变缓存被写入的方式
//=================================================================
App::uses('CacheHelper', 'View/Helper');
class MyCacheHelper extends CacheHelper {
/**
*
* 下面的 _writeFile() 函数几乎与原始文件完全相同,唯一的区别在于我们不直接写入文件,
* 而是使用 Cache::write() 来实现。
*
*/
protected function _writeFile($content, $timestamp, $useCallbacks = false) {
//在这点之下,代码和CakePHP中的方法一致
//============================================================
$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';
$file = '<!--cachetime:' . $cacheTime . '--><?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 = new CakeResponse();
$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;
//在这点之上,代码和CakePHP中的方法一致
//============================================================
$cacheEngine='_cake_views_';
$cacheKey = 'helper_cached_views_'.md5(CACHE . 'views' . DS . $cache);
if(!Cache::write($cacheKey, $file, $cacheEngine)) {
return NULL;
}
return $file;
}
}
需要使用自定义缓存分派器(调度器)是为了强制CakePHP从缓存引擎中而不是直接从硬盘上读取缓存信息:
<?php
/**
* CakePHP补丁:使用缓存引擎扩展CakePHP的缓存助件
* http://www.datumbox.com/
*
* Copyright 2013, Vasilis Vryniotis
* Licensed under MIT or GPLv3, see LICENSE
*/
//在 /app/Lib/Routing/Filter/MyCacheDispatcher.php 里我们改变缓存被写入的方式
//============================================================================================
App::uses('CacheDispatcher', 'Routing/Filter');
App::uses('ClassRegistry', 'Utility');
class MyCacheDispatcher extends CacheDispatcher {
public $priority = 8; //我们在它的父节点之前设定比它的父节点更小的优先级来运行这个分派器(调度器)
public function beforeDispatch(CakeEvent $event) {
//在这点之下,代码和CakePHP中的方法一致
//============================================================
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';
//在这点之上,代码和CakePHP中的方法一致
//============================================================
$cacheEngine='_cake_views_';
$cacheKey = 'helper_cached_views_'.md5($filename);
$data = Cache::read($cacheKey, $cacheEngine);
if ($data) {
$controller = null;
App::import('View','My');
$view = new MyView($controller);
$result = $view->renderCacheFromString($data, microtime(true));
if ($result !== false) {
$event->stopPropagation();
$event->data['response']->body($result);
return $event->data['response'];
}
}
return;
}
}
需要使用自定义试视图类是为了强制CakePHP从字符串而不是从文件解析缓存序列化对象:
<?php
/**
* CakePHP补丁:使用缓存引擎扩展CakePHP的缓存助件
* http://www.datumbox.com/
*
* Copyright 2013, Vasilis Vryniotis
* Licensed under MIT or GPLv3, see LICENSE
*/
//此 /app/View/MyView.php 文件扩展CakePHP的视图类
//======================================================
App::uses('View', 'View');
class MyView extends View {
/**
*
* renderCacheFromString() 与我们的 renderCache() 函数完全相同,
* 唯一不同的是我们不从文件中读取缓存的内容,而是在一个字符串变量中。
*
*/
public function renderCacheFromString($out, $timeStart) {
//和 renderCache() 很相似。唯一的区别是不是从文件中读取数据,而是直接从字符串中获取数据
ob_start();
//原始的 renderCache() 函数包含缓存文件。包含一个文件等价于评估字符串
eval("?>" . $out . "<?php "); //这太难看了!!!寻找替代品?
$out = ob_get_clean();
//在这点之下,代码和CakePHP中的方法一致
//============================================================
if (preg_match('/^<!--cachetime:(\\d+)-->/', $out, $match)) {
if (time() >= $match['1']) {
//@codingStandardsIgnoreStart
@unlink($filename);
//@codingStandardsIgnoreEnd
unset($out);
return false;
} else {
if ($this->layout === 'xml') {
header('Content-type: text/xml');
}
return substr($out, strlen($match[0]));
}
}
//在这点之上,代码和CakePHP中的方法一致
//============================================================
}
}
只需修改 bootstrap.php 即可加载新的CacheDispatcher(缓存分派器/调度器),并配置HTML页面的存储方式:
<?php
/**
* CakePHP补丁:使用缓存引擎扩展CakePHP的缓存助件
* http://www.datumbox.com/
*
* Copyright 2013, Vasilis Vryniotis
* Licensed under MIT or GPLv3, see LICENSE
*/
//在 /app/Config/bootstrap.php 里我们增加新的缓存分派器(调度器)和视图缓存配置
//=============================================================================================
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
//'CacheDispatcher', //注释掉默认的一个
'MyCacheDispatcher' //使用新的分派器(调度器)
));
//这就是您放置您为了视图而做的的缓存配置的地方
Cache::config('_cake_views_', array(
'engine' => $engine,
'prefix' => $prefix . 'cake_views_',
'path' => CACHE . 'views' . DS,
'serialize' => ($engine === 'File'),
'duration' => $duration
));
最后一步是告知您的 AppController 去使用您自定义的视图类和缓存助件:
<?php
/**
* CakePHP补丁:使用缓存引擎扩展CakePHP的缓存助件
* http://www.datumbox.com/
*
* Copyright 2013, Vasilis Vryniotis
* Licensed under MIT or GPLv3, see LICENSE
*/
//在 /app/Controller/AppController.php 里我们增加自定义视图类
//=================================================================
App::uses('Controller', 'Controller');
class AppController extends Controller {
public $viewClass = 'My'; //告知 AppController 有关新的视图类的信息
public $helpers=array( //告知 AppControlller 去使用新的缓存助件
'Cache' => array('className' => 'MyCache')
);
}
以上代码的想法非常简单。我们覆盖默认的缓存助件和缓存分派器(调度器)类,这些类负责直接从/向文件系统读取/写入HTML代码,并且让它们使用替代的缓存引擎。请注意,我们还需要扩展默认的视图类,以便从字符串而不是从文件解析缓存对象。上述解决方案中唯一难看的部分是使用PHP的 eval() 函数,在这种情况下,由于CakePHP存储缓存的方式,导致这是不可避免的。
免责声明:尽管提出的方案作为一个更大型应用程序的一部分经过严格的测试,但上述代码片段仅作为CakePHP框架开发人员的概念验证而做,因此未经测试。可能需要稍作修改才能使代码正常工作。使用此代码需要您独自承担风险。
对于那些厌倦了复制粘贴代码的人,我已经创建了这些文件,并把它们放在一个zip文件中(这不是很好吗?)。要使用补丁程序,只需将文件复制到相应的文件夹中,然后按照上文所述修改相应的文件。你可以在这里下载代码
如果您喜欢这篇文章,请将它分享到社交媒体上,我保证在将来会发布更多的文章。
Vasilis Vryniotis是数据科学家,软件工程师,Datumbox机器学习框架的作者,也是一个引以为傲的极客。了解更多
① CacheHelper:官方文档中文版将 Helper 译为【助件】,参考官方Views部分译文。百度百科则将其以为【助手】,参考链接。
② CakePHP现已推出3.x版本。