发新话题
打印

PHP模板引擎比较和给PHPLIB增加cache缓存功能

本主题由 phpres 于 2007-10-16 16:00 移动

PHP模板引擎比较和给PHPLIB增加cache缓存功能

PHP下的模板解决方案很多,主流的有 PHPLIB、IT、FlexySmarty等,这些模板引擎各有所长,很多人推崇Smarty,根据个人使用感受来看,Smarty有以下特点:

1、模板里面支持语法丰富,方便“程序员”(注意)在模板中实现丰富灵活的逻辑;
2、使用“预编译模板”的概念,能使性能得到一定提升;
3、支持Cache功能。

  这几个特点我认为是最核心的部分,另外Smarty还提到所谓的模板FrameWork,个人认为价值不大,并非一个完整的PHP FrameWork,所以并不推崇。而就前面Smarty的几个特点来说,反倒成为我不选择Smarty的原因之一。


1、作为模板最大的作用就是MVC框架支持,Smarty的模板语法过分丰富,导致模板在View层与Model和Controller层模糊不清,在实际的PHP应用开发中,程序员实际上过多的参与到了View层的工作,不利于团队的分工。

2、而Smarty提供的“预编译模板”的功能,表面上提高了性能,但是在实际应用中,并非完全如此,由于PHP本身属于解释性语言,而Smarty的预编译功能也是使用了PHP本身来开发,并没有使用C/C++来开发成PHP的扩展,从性能上来讲,编译的开销会比较大,如果内容更新较频繁的应用,这样的功能并不适用,反倒增加性能开销,再有Smarty会把一个简单的模板和应用,编译后生成好几个复杂的php文件,对于存储成本的和性能成本的增加也是有影响的;

3、Smarty本身太大,核心的类文件达到160多k,如果加上Core和plugin的类,达到300多k的代码,每次用户请求产生的系统开销是很大的,比较适合中小应用。相比之下,PHPLib类仅一个文件14k左右,Flexy也很小,更适合大型应用。

  相比Smarty,个人更推荐使用Flexy和PHPLIB,Flexy具备了Smarty提供的大部分功能,也是预编译,但是性能经过我测试比Smarty要好,也同样有丰富的语法,对于喜欢同时开发PHP代码和模板文件的程序员来说,值得推荐。而PHPLIB是我最推荐使用的,主要考虑这样几个因素:

1、PHPLIB有很久远的历史,它的前身是Perl的模板引擎,然后迁移到php3时代的phplib里面,接而进入到PHP的PEAR框架下,从稳定性来说不用怀疑;

2、PHPLIB相当简单,全部代码一个类,仅14K左右,提供了最基本的MVC解决思路,虽然没有灵活多样的模板语法支持,但是根据我的经验,WEB应用上使用模板解决的地方,PHPLIB都能实现,同时由于代码量非常小,系统开销也会小;

3、性能优秀,网上曾经有人做过PHPLIB和Smarty的性能测试,在Smarty不打开编译缓存功能的情况下,PHPLIB比Smarty快20%,Smarty打开编译缓存情况下,比PHPLIB快10%,虽然这个测试我认为并不充分,并且和实际应用有差距,但是我认为至少从性能上来说,PHPLIB并不比Smarty慢,相反我认为在实际WEB应用中会更快,如果给PHPLIB增加一个Cache功能,那么性能能提升将近10倍(这个结果是我自己测试后得到的结果);

4、PHPLIB在模板的解析和实现上与别的模板没有什么差别,大家都是使用字符串替换操作来实现,而且都使用了preg_replace()函数来实现。

  通过前面的个人分析(个人意见,大家可以自己实际测试,也可以否定我的说法),我决定在我的WEB应用中使用基于PHPLIB的模板引擎来实现MVC模式,不过由于PHPLIB过分简单,不提供Cache功能,所以我自己动手扩展了PHPLIB类,增加缓存的实现,实际使用后发现效果非常好。

在我扩展的类 MyTemplate 中,主要实现下面功能需求:
1、自定义缓存开关
2、支持缓存超时判断
3、支持模板文件更新后更新缓存判断
4、支持程序文件更新后缓存判断
5、支持缓存文件散列存放自定义

  下面是我的 MyTemplate 类实现代码,里面包含了具体的使用说明和范例,有不明白的地方可以留言给我,欢迎大家和我交流PHPLIB相关的模板应用,和Smarty、Flexy相关的内容就希望别问我,本人不对此类问题给予回答,谢谢。
一個偽裝成白癡的天纔!

TOP

复制内容到剪贴板
代码:
<?php
//
// +----------------------------------------------------------------------+
// | PHP version > 4.3.4 & 5.x                                            |
// +----------------------------------------------------------------------+
// | Copyright (c) 2006-2007 toplee.com                                   |
// +----------------------------------------------------------------------+
// | 本文件包含PEAR的PHPLIB模板类扩展功能类定义                           |
// | 本类包含对PEAR的PHPLIB Template类继承,并实现cache、utf8等支持       |
// +----------------------------------------------------------------------+
// | Authors: Michael Lee <webmaster@toplee.com>                          |
// +----------------------------------------------------------------------+
//
// $Id: MyTemplate.class.php,v 1.0 2006/08/28
//

/**
* 使用帮助:
* 实例化类     $TPL = new MyTemplate(array $tpl_config)
* 调用模板     $TPL->setFile($handle, $filename="")
* 设置block    $TPL->setBlock($parent, $handle, $name="")
* 解析变量     $TPL->setVar($varname, $value="", $append=false)
* 解析页面     $TPL->parse($target, $handle, $append=false)
* 输出页面     $TPL->p($varname) = echo $TPL->get($varname);
* 检查cache    $TPL->cacheCheck()
* 写入cache    $TPL->cache($data)
* 输出cache    $TPL->pCache()
*
* 实例化模板类时配置选项$tpl_config格式和说明
* $tpl_config = array(
*  'debug'     => false,   //是否显示debug信息
*  'root'      => 'tpl',   //模板存放路径,目前是相对路径,末尾不包含/
*  'unknowns'  => 'remove',//模板中未解释的标记是否保留输出
*  'cache'     => array(
*      'cache'     => true,        //是否打开cache支持
*      'root'      => 'tpl_cache/',//cache存放路径,目前支持相对路进,末尾包含/
*      'hash'      => 3,           //cache缓存文件散列目录级数,比如 a/3/d
*      'life_time' => 10,          //cache文件默认失效时间,单位秒
*      'file_ext'  => 'tpl.html',  //cache文件扩展名
*      ),
*  );
*/

require_once('HTML/Template/PHPLIB.php');

class MyTemplate extends Template_PHPLIB
{
    var $cache = array();   //和cache相关的配置信息,在tpl_config里面设置

    var $cache_dir = '';    //当前请求对应的cache存放目录,相对路径末尾包含 /
    var $cache_md5 = '';    //根据script_path得到的md5值,用于路径和cache文件名
    var $cache_file = '';   //包含完整路径的cache文件
    var $cache_ini = '';    //包含网站路径的cache配置文件

    var $root_path = '';    //从当前路径开始相对于网站根目录的路径信息
    var $script_path = '';  //当前请求页面的绝对路径,如/test.php?a=b,支持POST
    var $php_self = '';     //当前请求页面的绝对路径,如/test.php

    /**
     * $cache_parse = array(
     *          'md5'       => '',
     *          'tpl_count' => 2,
     *          0   => array(
     *                  'tpl' => 'aa.tpl',
     *                  'md5' => 'md51',
     *                  ),
     *          1   => array(
     *                  'tpl' => 'bb.tpl',
     *                  'md5' => 'md52',
     *                  ),
     *          );
     */
    var $cache_parse = array();

    /**
     * @Purpose:构造函数
     * @Param array $tpl_config
     * @Author Michael Lee <lijl@lesoo.com>
     * @Return: NULL
     */
    function MyTemplate($tpl_config="")
    {
        $debug = isset($tpl_config['debug']) ? $tpl_config['debug'] : false;
        $root = isset($tpl_config['root']) ? $tpl_config['root'] : ".";
        $unknowns = isset($tpl_config['unknowns']) ? $tpl_config['unknowns'] : 'remove';
        $this->cache = $tpl_config['cache'];

        $this->Template_PHPLIB($root,$unknowns);
        $this->debug = $debug;

        //仅在打开了cache支持的配置情况下才启用和cache相关的功能
        if ($this->cache['cache']) {
            //初始化得到一些当前访问请求对应的路径信息
            $this->_getScriptPath();
            $this->_getRootpath();
            $this->_getPhpSelf();

            //取得当前请求对应的cache_dir和cache_md5
            $this->_getCacheFile();
        }

        if ($this->debug) {
            if ($this->cache['cache'])
                echo '<font color=red>Current Cache md5:'.$this->cache_md5.'<br></font>';
            else
                echo '<font color=red>Cache Disabled! Just use PHPLIB!<br></font>';
        }
    }


    /**
     * 把当前访问请求结果页面写入cache
     * 首先要得到当前页面根据php_self得到的md5_file值
     * 然后根据$this->file数组得到各个模板文件名和路径,分别得到他们的md5_file值
     * 把以上值写入到cache的ini配置文件,同时把cache页面内容写入cache文件
     *
     * @access public
     * @param string $data
     * @return true
     */
    function cache($data)
    {
        if (!$this->cache['cache']) return;

        $ini = "";
        $file = $this->php_self;
        //为了支持cli-cgi模式的php运行环境
        if (substr($file,0,1) == "/") $file = $this->root_path.$file;
        $md5 = md5_file($file);
        $ini .= "md5 = $md5\r\n";

        $count = count($this->file);
        $ini .= "tpl_count = $count\r\n";

        $i = 0;
        foreach ($this->file AS $k=>$v) {
            $ini .= "[$i]\r\n";
            $ini .= "tpl = \"$v\"\r\n";
            $ini .= "md5 = \"".md5_file($v)."\"\r\n";
            $i++;
        }
        //echo $ini; exit;  //for debug

        $this->_mkdir2($this->cache_dir);
        if (!$this->_writeToFile($this->cache_ini,$ini)) return false;
        if (!$this->_writeToFile($this->cache_file,$data)) return false;

        return true;
    }



    /**
     * 检查当前请求相关的cache是否可用
     * 1.$this->cache['cache']是否为true 2.是否存在 3.是否过期
     * 4.是否当前php程序文件有改变 5.是否相关的tpl文件有变动
     *
     * @access public
     * @return bool true/false
     */
    function checkCache()
    {
        //1.判断当前是否允许cache
        if (!$this->cache['cache']) {
            if (DEBUG) echo 'cache disabled';
            return FALSE;
        }

        //2.判断当前cache_file是否存在
        if (!file_exists($this->cache_file) || !file_exists($this->cache_ini)) {
            if (DEBUG) echo 'cache not exists';
            return FALSE;
        }

        //3.判断是否过期
        $now = time();
        $orig = filemtime($this->cache_file);
        $chk = $now-$orig;
        if ($chk > $this->cache['life_time']) {
            if (DEBUG) echo 'cache expired';
            return FALSE;
        }


        //解析cache的ini配置文件
        $this->_parseCacheIni();


        //判断当前php文件是否有改变
        //取得当前php文件的md5_file值
        $php_self = $this->root_path.substr($this->php_self,1);
        $md5_script = @md5_file($php_self);
        if ($md5_script != $this->cache_parse['md5']) {
            if ($this->debug) echo 'md5 bad';
            return false;
        }


        //判断相关的tpl文件是否有变化,方法和前面类似
        for($i=0;$i<$this->cache_parse['tpl_count'];$i++) {
            $tpl_file = $this->cache_parse[$i]['tpl'];
            $tpl_md5 = $this->cache_parse[$i]['md5'];
            $tpl_md5_cur = @md5_file($tpl_file);
            if ($tpl_md5_cur != $tpl_md5) {
                if ($this->debug) echo 'tpl md5 bad: '.$tpl_file.$tpl_md5_cur.'#'.$tpl_md5;
                return false;
            }
        }

        return true;
    }


    /**
     * 取得cache,用于页面输出
     * 去掉cache前面的配置行
     *
     * @access public
     * @return true
     */
    function pCache()
    {
        @readfile($this->cache_file);
        return true;
    }

    /**
     * 删除cache
     *
     * @access public
     * @return NULL
     */
    function rmCache()
    {
        @unlink($this->cache_file);
        @unlink($this->cache_ini);
        return;
    }


    /**
     * 得到cache文件名,包括路径信息,如./cache/ab/cd/ef/abcdef.....tpl.html
     *
     * @access private
     */
    function _getCacheFile()
    {
        if (empty($this->script_path)) $this->_getScriptPath();
        $md5_string = md5($this->script_path);
        $path = "";
        for ($i=0;$i<$this->cache['hash'];$i++)
            $path .= substr($md5_string,$i,1).'/';

        $path = $this->cache['root'].$path;
        $this->cache_dir    = $path;
        $this->cache_md5    = $md5_string;
        $this->cache_file   = $path.$md5_string.'.'.$this->cache['file_ext'];
        $this->cache_ini    = $path.$md5_string.'.ini';
    }


    /**
     * 解析cache文件的头四行,得到下列信息
     * $this->cache_file_parse['php_self_md5'] 当前php程序的原始md5值
     * $this->cache_file_parse['tpls_count']和['tpls']
     *
     * @access private
     */
    function _parseCacheIni()
    {
        $this->cache_parse = @parse_ini_file($this->cache_ini,true);
        return true;
    }


    /**
     * 把内容写入指定文件
     *
     * @access private
     */
    function _writeToFile($file,$content,$mode='w')
    {
        $oldmask = umask(0);
        $fp = fopen($file, $mode);
        if (!$fp) return false;
        @fwrite($fp,$content);
        @fclose($fp);
        @umask($oldmask);
        return true;
    }

    /**
     * 创建多级目录
     *
     * @access private
     */
    function _mkdir2($dir)
    {
        $dir = @preg_replace("/\\\/","/",$dir);
        $dir = @preg_replace("/\/{2,}/","/",$dir);
        $dir = @explode('/',$dir);
   
        $path = "";
        for ($i=0;$i<count($dir);$i++) {
            $path .= $dir[$i].'/';
            if (!is_dir($path)) @mkdir($path,0700);
        }
        return true;
    }


    /**
     * 取得当前请求的完整路径,用于标示当前cache的唯一性
     * 目前使用public_setting.inc.php里面取得的SCRIPT_PATH值
     * 注意:如果web请求通过POST方式进行的提交,那么可能造成结果页面cache有问题
     * 处理POST的方法,就是在SCRIPT_PATH的基础上,判断$_POST变量是否为空
     * 如果不为空,则进行serialize()处理,保证post的唯一性
     *
     * @access private
     */
    function _getScriptPath()
    {
        global $_SERVER,$_POST,$_ENV;
        if ($_ENV['REQUEST_URI'] OR $_SERVER['REQUEST_URI']) {
        $sp = $_SERVER['REQUEST_URI'] ? $_SERVER['REQUEST_URI'] : $_ENV['REQUEST_URI'];
        } else {
            if ($_ENV['PATH_INFO'] OR $_SERVER['PATH_INFO']) {
                $sp = $_SERVER['PATH_INFO'] ? $_SERVER['PATH_INFO']: $_ENV['PATH_INFO'];
            } else if ($_ENV['REDIRECT_URL'] OR $_SERVER['REDIRECT_URL']) {
                $sp = $_SERVER['REDIRECT_URL'] ? $_SERVER['REDIRECT_URL']: $_ENV['REDIRECT_URL'];
            } else {
                $sp = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF'];
            }
        
            if ($_ENV['QUERY_STRING'] OR $_SERVER['QUERY_STRING']) {
                $sp .= '?' . ($_SERVER['QUERY_STRING'] ? $_SERVER['QUERY_STRING'] : $_ENV['QUERY_STRING']);
            }
        }
   
        $sp = preg_replace("/\/{2,}/","/",$sp);
        $find = array('"', '<', '>');
        $replace = array('"', '<', '>');
        $sp = str_replace($find, $replace, $sp);
        $sp = xss_clean($sp);
        
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $p = @serialize($_POST);
            $sp .= "?$p";
        }
        return $sp;
    }

    /**
     * 取得当前请求的脚本文件绝对路径
     * 用于得到当前文件的md5值,判断当前文件是否被修改过
     * 当前使用public_setting.inc.php里面得到的PHP_SELF值
     *
     * @access private
     */
    function _getPhpSelf()
    {
        global $_ENV,$_SERVER;
        
        if ($_ENV['PHP_SELF'] OR $_SERVER['PHP_SELF'])
            $p = $_ENV['PHP_SELF'] ? $_ENV['PHP_SELF'] : $_SERVER['PHP_SELF'];
        elseif ($_ENV['SCRIPT_NAME'] OR $_SERVER['SCRIPT_NAME'])
            $p = $_ENV['SCRIPT_NAME'] ? $_ENV['SCRIPT_NAME'] : $_SERVER['SCRIPT_NAME'];
        else
            $p = preg_replace('#(\?.*)#', '', $this->script_path);
            
        return $p;
    }

    /**
     * 取得当前请求的脚本文件相对于根目录的相对路径,如 ../../
     * 当前使用public_setting.inc.php里面得到的ROOT_PATH值
     *
     * @access private
     */
    function _getRootPath()
    {
        $a = @explode("/",$this->script_path);
        $c = @count($a);
        $p = "";
        for ($i=0; $i < $c-2; $i++) $p = "../".$p;
   
        if ($p == "") $p="./";
        
        return $p;
    }
}
?>
一個偽裝成白癡的天纔!

TOP

发新话题