php模板引擎的原理与简单实例

模板引擎其实就是将一个带有自定义标签的字符串,通过相应的规则解析,返回php可以解析的字符串,这其中正则的运用是必不可少的,所以要有一定的正则基础。
总体思想,引入按规则写好的模板,传递给标签解析类(_HtmlTag)进行解析,再把解析好的字符串传递给php进行解析渲染输出
首先定义了一个_HtmlTag类:

 

1
2
3
4
5
6
7
8
9
10
class _HtmlTag{
  protected $taglist = "if|elseif|else|loop|for|while|=|:=|:e|:html|:";
  protected $string;
  protected $tpldir;
  function __construct($dir, $tpl) {
    $this->findReg = "/\\{[ ]*($this->findReg)(\\s*[\\{\\}]+\\s*)?\\}/i";
    $this->tpldir = $dir;
    $this->tpl =$tpl;
  }
}
  • 先解析下render方法
    在render方法中对传入的字符串$s,进行相应的替换渲染,用到了extends方法,import方法,''开头的各种过滤方法,后面会一一进行详解.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function render($s) {
    $s = str_replace(['<--{', '}-->'], ['{','}'], $s);
    if (strpos($s, "{extends") !== false) {
      $s = $this->_extends($s);
    }
    $s = preg_replace('/[\\n\\r]+/s', "\\n", $s);
    $s = preg_replace('/\\r+/s', "", $s);
    $s = preg_replace('/\\t+/s', "\\t", $s);
     
    $this->string = str_replace(["\\{", "\\}"], ["\\HKH1", "\\HKH2"], $s);
    preg_match_all("/\\{[ ]*(import)(\\s*[^\\{\\}]+\\s*)?\\}/i", $s, $d);
    foreach($d[0] as $k => $v) {
      call_user_func_array(array($this, "_import"), array($v, $d[1][$k], $d[2][$k]));
    }
    static $m = array("=" => "_defaultEcho", ":=" =>" _xssEcho", ":e" => "_htmlencodeEcho", ":" => "_echo", ":html" => "_htmldecodeEcho");
    preg_match_all($this->findReg, $this->string, $d);
    foreach ($d[0] as $k => $v) {
      if (isset($m[$d[1][$k]])) {
          $medth = $m[$d[1][$k]];
       } else {
          $medth = '_' . $d[1][$k];
       }
        call_user_func_array(array($this, $medth), array($v, $d[1][$k], $d[2][$k]));
    }
    }
  • _extends方法

    使用 个方法是为了判断是否有继承相应的模板文件,在子文件中用{extends 模板名},一般只要填写文件名,如index,系统会自动去构造完整的文件路径,一般在上一级目录,这个在realTpl()方法时会有详解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function _extends($tpl) {
    preg_match("/\\{\\s*extends\\s+([^\\{\\}]+?)\\s*\\}/i", $tpl, $d);
    list($search, $arg) = $d;
    if (stripos($arg, ".") !==0 ) {
      $arg = '../' . $arg;
    }
    $file = $this->tpl->me->realTpl($arg . ".htm");
    $basetpl = file_get_contents($file);
    preg_match_all("/\\{block\\s+([^\\{\\}]+?)\\s*\\}(.+)\\{\\/block\\}/s", $tpl, $ds);
    foreach($ds[1] as $n => $name) {
      $basetpl = preg_replace("/\\{block\\s+name={$name}\\s*\\}/", $ds[2][$n], $basetpl);
    }
    $basetpl = preg_replace("/\\{block\\s+name=.+?\\}/", ' ', $basetpl);
    return $basetpl;
    }
  • _import 方法

    _import 方法是用来对{import 文件名}标签进行解析,默认也是上层目录,会把该文件的内容解析到当前标签位置,同时支持使用:函数名|参数1, 参数2,...的方式进行字符串的回调.这中的display方法后面会有详解
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function _import ($search, $tag, $arg) {
    $arg = trim($arg);
    if (stripos($arg, ".") !== 0) {
      $arg = '../' . $arg;
    }
    $file = $this->tpl->me->realTpl($arg . ".htm");
    if (file_exists($file)) {
      $this->string = str_replace($search, file_get_contents($file), $this->string);
    return;
    } else {
        if (stripos($file, "$") !== false) {
            $this->string = str_replace($search, '<? include $this->tpl->display("' . $arg . '"); ?>', $this->string);
             return;
        }
        if (stripos($arg, '|') !== false) {
           list($func, $tmp) = explode("|", trim($arg));
           $kw = explode(', ', $tmp);
        } else {
           $func = trim($arg);
            $kw = array(); 
        }
        if (function_exists($func) {
          $tpl_str = call_user_func_array($func, $kw);
           $this->string = str_replace($search, $tpl_str, $this->string);
            return;
        } else {
          $this->string = str_replace($search, $arg, $this->string);
        return;
        }
        system_error($this->tpldir . trim($arg) . ".htm不存在");
    }
    }
  • 现在来看一个标签函数_loop,将标签{loop $data $k $v}或{loop $data $v}替换,实现模板循环,其中的parseAttr方法接下来将详解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function _loop($search, $tag, $arg) {
    list($attr, $arg) = $this->parseAttr($arg);
    $d = preg_split("/\\s+/", trim($arg));
    if (count($d) == 3) {
       $data = $d[0];
       $k = $d[1];
       $v = $d[2];
       $s = "<? \\n if(!empty($data)) {\\n \\t foreach(" . $data . " as {$k} => {$v}){";
    } elseif (count($d) == 2) {
      $data = $d[0];
      $v = $d[1];
      $s = "<? \\n if(!empty($data)){\\n \\t
          foreach(". $datsa . " as {$v}) {";
    }
     
    if (isset($attr['counter'])) {
      $s = "\\n \\t\\t<? if (!isset({$attr['counter']})) { {$attr['counter']} = 0;} ?> \\n \\t\\t" . $s . "\\n \\t\\t{$attr['counter']}++; \\n ?>;
    } else {
      $s .= "?>";
    }
    $this->string = str_replace($search, $s, $this->string);
    $this->string = str_replace("{/loop}", "<? \\t}\\n}?>", $this->string);
    }
  • parseAttr方法

    该方法用来解析标签 的参数,返回的第一个参数是值形式字符串,第二个是键值数组,如{loop $data $k $v}返回$data $k $v 组成 的字符串和一个空数组,{if $k = $v}则返回空字符串和和数组[$k => $v];
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function parseAttr($s) {
    $reg = '/([a-zA-Z0-9_])+\\s*=\\s*([a-zA-Z0-9_\\$]+)/i';
    $arg = preg_replace($reg, '', $s);
    preg_match_all($reg, $s, $d);
    $arr = array();
    foreach($d[1] as $key => $value) {
      $arr[trim($value)] = trim($d[2][$key]);
    }
    return array($arr, $arg);
    }
  • _if 方法

    将{if}{/if}标签解析成原生 的php,简单明了,不多解释
    1
    2
    3
    4
    5
    function _if ($search, $tag, $arg) {
    $replace[0] = "<? if($arg){?>";
    $replace[1] = "<? }?>";
    $this->string = str_replace(array($search, "{/$tag}"), $replace, $this->string);
    }
  • _else方法
    将{else}解析成原生的php
    1
    2
    3
    function _else($search, $tag, $arg) {
    $this->string = str_replace("{else}", "\\n" . '<? } else {?>', $this->string);
    }
  • _elseif方法
    将{elseif}解析成原生的php
    1
    2
    3
    function _elseif($search, $tag, $arg) {
    $this->string = str_replace($search, "<? }elseif($arg){?>", $this->string);
    }

      

  • _for 方法
    1
    2
    3
    4
    5
    function _for($search, $tag, $arg) {
    $s = trim(trim(preg_replace('/\\s+/', ' ; ', $arg)), ';');
    $this->string = str_replace($search, "<? for($s){?>", $this->string);
    $this->string = str_replace("{/for}", "<? \\t\\n}?>", $this->string);
    }

      

  • _while方法
    1
    2
    3
    4
    function _while($search, $tag, $arg) {
    $this->string = str_replace($search , "<? while($aeg){ ?>", $this->string);
    $this->string = str_replace("{/while}", "<? \\t\\n} ?>", $this->string);
    }

      

  • _end方法
    通过这个方法对其$this->string进行修改可以对变量进行如下标签定义:
    {$val}{$arr[$key]}{$arr['key']}{:val()}{$arr.key}{$arr.$key}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function _end() {
    //{$val}标签解析
    $this->string = preg_replace('/\\{(\\$\\w+.*)\\}/is', "<?=\\\\1;?>", $this->string);
     //{:$val()}标签解析
    $this->string = preg_replace('/\\{\\:\\s*(\\$?\\w+.*?)\\}/is', "<? =\\\\1;?>", $this->string);
    //将$val[arg]解析成$val['arg']
    $this->string = preg_replace('/\\$([_a-z]+\\w*)\\[([_a-z]+\\w*)\\]/is', "$\\\\1['\\\\2']", $this->string);
    //支持点号访问数组。如array['key']可以用array.key访问
    $this->string = preg_replace('/\\$([_a-z]+\\w*)\\.(\\$[_a-z]+\\w*)/is', "$\\\\1[\\\\2]", $this->string);
    $this->string = preg_replace('/\\$([_a-z]+\\w*)\\.([_a-z]+\\w*)/is', "$\\\\1['\\\\2']", $this->string);
    $this->string = preg_replace(array("/<\?/", "/<?\php\s*=/"), array("<?php ", "<?php echo "), $this->string);
    }

      

  • _result方法,做最后处理,返回字符串,_end方法在上面已经解释过了

    1
    2
    3
    4
    5
    function result() {
    $this->_end();
    $this->_baseParse($this->stirng);
    return str_replace(array("\\HKH1", "\\HKH2"), array("\\{", "\\}"), $this->string);
    }

      

    下面再来介绍一下几个过滤的方法

  • _htmlencodeEcho方法

    可以用此种形式对变量进行htmlspecialchars式的过滤spspecialchars函数后面会有介绍{:e $val}

    1
    2
    3
    4
    5
    6
    7
    function _htmlencodeEcho($search, $tag, $arg) {
    if (stripos($arg, '$')) {
      $this->string = str_replace($search, "<? echo sphtmlspecialchars(!empty($arg)?$arg:''))?>", $this->string);
    } else {
      $this->string = str_replace($search, "<? echo sphtmlspecialchars($arg)?>", $this->string);
    }
    }

      

  • _htmldecodeEcho方法
    1
    2
    3
    4
    5
    6
    7
    _htmldecodeEcho($search, $tag, $arg) {
    if (stripos($arg, '$')) {
      $this->string = str_replace($search, "<? echo htmlspecialchars_decode(!empty($arg)?$arg:'')?>", $this->string);
    } else {
      $this->string = str_replace($search, "<? echo htmlspecialchars_decode($arg)?>", $this->string);
    }
    }

      

  • _defaultEcho方法
    1
    2
    3
    4
    5
    6
    7
    function _defaultEcho($search, $tag, $arg) {
    if (stripos($arg, ',') == false) {
      $arg . = ", ' ' ";
      list($v, $default) = explode(",", $arg);
      $this->string = str_replace($search, "<? echo empty($v)?$default:$v; ?>", $this->string);
    }
    }

      

  • _xssEcho方法

    1
    2
    3
    _xssEcho($search, $tag, $arg) {
    $this->string = str_replace($search, "<? echo xssRemove($arg); ?>", $this->string);
    }

      

    _echo方法

    1
    2
    3
    function _echo($search, $tag, $arg){
    $this->string = str_replace($search, "<? echo $arg;?>", $this->string);
    }

      

  • _baseParse方法,这个对css和js的引入进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function _baseParse($s) {
    if (strpos($s, "{loadCss") !== false) {
      $s = $this->_loadCss($s);
    }
    if (strpos($s, "{loadJs") !== false) {
      $s = $this->_loadJs($s);
    }
    return $s;
    }

      

  • _loadJs方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    function _loadJs($tpl) {
    preg_match_all('/\{loadJs\s+([^\{\}]+?)\s*\}/i', $tpl, $match);
    include "jsmin.php";
    foreach($match[1] as $k => $js) {
      $jsstr = "";
      $cdn = "";
      $if (stripos($js, ',') !== false) {
          $filename = "";
          $md5 = "";
          $js_content = "";
          foreach(explode(',', $js) as $j) {
            $filename .= basename($j, '.js') . ',';
            $md5 .= self::fileMd5($j);
            if (stripos($j, '.min.') == false) {
              $js_content .= JSMin::minify(file_get_contents(WEB_ROOT . $j));
            } else {
              $js_content .= file_get_contents(WEB_ROOT . $j);
            }
            $js_content .= ';'
          }
        $md5 = md5($md5);
        $filename = dirname($j) . '/' .trim($filename, ',') . '.js';
        self::parseJs($js_content, $filename);
        $jsstr = "<script src=\"{$cdn}{$filename}?v={$md5}\"></script>";
      } else {
        $md5 = self::fileMd5($js);
        if (stripos($js, ".min.") === false) {
          $js = self::pareJs(JSMin::minify(file_get_contents(WEB_ROOT . $js)), dirname($js). '/' .basename($js, '.js') . '.min.js');
        }
        $jsstr = "<script src=\"{$cdn}{$js}?v={$mdt}\"></script>";
      }
      $tpl = str_replace($match[0][$k], $jsstr, $tpl);
    }
    return $tpl;
    }

      

    _loadCss方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    function _loadCss($tpl) {
    preg_match_all('/\{loadCss\s+([^\{\}]+)\s*\}/i' , $tpl , $match);
    $cdn = "";
    foreach($match[1] as $k => $css) {
      $cssstr = "";
      if (stripos($css, ',') !== false) {
        $filename = "";
        $md5 = "";
        $css_content = "";
         foreach($explode(',', $css) as $css) {
             $filename .= basename($css, '.css') . ',';
             $md5 = self::fileMd5($css);
              $css_content .= file_get_contents(WEB_ROOT .$css);  
         }
         $md5 = md5($md5);
         $filename = dirname($css) . '/' .trim($filename, ',') . '.css';
          self::parseCss($css_content, $filename);
           $cssstr = "<link rel=\"stylesheet\" href=\"{$cdn}{$filename}?v={md5}\">";
      } else {
        $md5 = self::fileMd5($css);
         if (stripos($css, ".min.") == false) {
            $css = self::parseCss(file_get_contents(WEB_ROOT . $css), dirname($css) . '/' .basename($css, '.css') . '.min.css');
          }
          $cssstr .= "<link rel=\"stylesheet\" href=\"{$cdn}{$css}?v={$md5}\">";
      }
      $tpl = str_replace($match[0][$k], $cssstr, $tpl);
    }
    return $tpl;
    }

      

  • 上面两个函数都用到了各自的parseCss,和parseJs,让我们来看下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static function parseCss($css_content, $filename) {
    $css_content = preg_replace("/[\r\n\t]/", '', $css_content);
    $css_content = preg_replace("/ +/", ' ', $css_content);
    sp_file_pu_contents(WEB_ROOT . $filename, $css_content);
    return $filename;
    }
    static function parseJs($js, $filename) {
    sp_file_put_content(WEB_ROOT . $filename, $js);
    return $filename;
    }

      


上面的_html类和他的方法对传入的字符串进行了标签到原生 Php的替换,可以传入相应的模板,也可以在其中自定义自己的过滤方法。

下面我们再来看一个template类:

定义这个类来对模板进行管理,这里会用到上面的_html类,基本定义如下,接下来会介绍其中的一些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class template {
  private $tpl_cache_dir;
  public $ext = ".htm";
  public $default_tpl_dir = false;
  public $me;
 
  function _construct($tpl_cache_dir, $me) {
    $this->sets($tpl_cache_dir);
    $this->me = $me;
  }
  function sets($tpl_cache_dir) {
    $this->tpl_cache_dir = $tpl_cache_dir;
    if (SP_DEBUG == 1) {
      spmkdir($this->tpl_cache_dir);
    }
  }
  function default_template($dir) {
    $this->default_tpl_dir = $dir;
  }
}

  

  • getString方法

    在之前 开启ob_start(),然后获取这之间的ob数据,试用于框架中

    1
    2
    3
    4
    5
    function getString() {
    $s = ob_get_contents();
    ob_clean();
    return $s;
    }

      

  • display方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function display($tpl, $return = 0, $script = SCRIPT) {
    $tpl .= $this->ext;
    $tpl_name = $this->tpl_cache_dir . $script . '_' . str_replace(['/', '\\\\'], '_', $tpl);
    if (SP_DEBUG){
      if (SP_DEBUG == 1 || (SP_DEBUG == 2 && !file_exists($tpl_name))) {
        $this->compiles($tpl_name, $tpl);
      }
    }
    return $tpl_name;
    }

      

  • compiles方法, 在这里使用了sp_file_put_contentsb函数会在后面介绍
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function compiles($cache_name, $tpl) {
    if (!file_exists($tpl)) {
      if (file_exists($this->default_tpl_dir . $tpl)) {
        $html = file_get_contents($this->default_tpl_dir . $tpl);
      } else {
        spmkdir(dirname($tpl), '777');
        file_get_contents($tpl, 这个是自动生成模板');
      }
    } else {
      $html = file_get_contents($tpl);
    }
    $tag = new _HtmlTag(realpath(dirname($tpl) . '/../'), $this);
    $tag->render($html);
    $html = preg_replace_callback('//s', array(&$this, "make_magic_func"), $tag->result());
    $html = preg_replace('/[\\n\\r]{1,}/s', "\\n", $html);
    @unlink($cache_name);
    sp_file_put_contents($cache_name, $thml);
    }

      

  • make_magic_func方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function make_magic_func($d) {
    preg_match_all('/(\\w+)=\\s*(\\'|\\")([^\\'"]+)?\\2/m', $d[2], $dd);
    $aa = array();
    foreach($dd[1] as $i => $k) {
      $vs = preg_replace('/([^\\-]*[<>]{1,1})\\s*(\\S+)?\\s*/', '\\1\\'\\2\\' ', $dd[3][$i]);
      $vs = preg_replace('/=\\s*(\\S+)?\\s*/', '=\\'\\1\\'', $dd[3][$i]);
      if (stripos($d[3][$i], '$') !== false) {
        $vs = preg_replace('/\\$([^\\']+)\\s*/', '{$\\1}', $vs);
       }
    }
    }

    注意

  • 两个类的相应方法都 定义好了,这其中用到的函数sp_file_pu_contents($file, $data)
    1
    2
    3
    4
    5
    6
    7
    8
    function sp_file_put_contents($file, $data) {
      $dir = dirname($file);
      if (is_writeable($dir)) {
          return file_put_contents($file, $data);
      } else {
          throw new writeFail(sprintf("写入文件%s,失败,目录不可写", $file), 503);
      }
    }
    js压缩类: jsmin.class.php
    链接:http://pan.baidu.com/s/1kUBx0X5 密码:y5jb
    将这两个 类定义在一个文件中,使用进进行相应的配置
    1
    define("WEB_ROOT", dirname(__FILE__) . '/');

   使用

  • 文件准备

    1
    base.htm,main.htm,test.js,test1.js,test.css,test1.css,indx.php,im.htm
  • main.htm:

    1
    2
    3
    4
    {import ./im}
    {block name=head}
    {block name=body}
    {block name=foot}
  • base.htm
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    {extends ./main}
    {block head}
    {loadCss test.css,test1.css}
    {loop $data $k $v}
    <p>
    我是头部
    </p>
    {/loop}
    {if $bb == 'aa'}
    <p>
    我是if 测试
    </p>
    {elseif $bb = 'bb'}
    <p>
    我是elesif测试
    </p>
    {else}
    <p>
    我是eles测试
    </p>
    {/if}
    {for $i=0 $i<10 $i++}
    <p>
    我是for循环
    </p>
    {/for}
    {while $con >5}
    <? $con--;?>
    <p>
    我是while循环
    </p>
    {/while}
    {/block}
    {block foot}
    <p>
    我是尾部
    </p>
    {loadJs test1.js,test.js}
    {/block}
  • im.htm
    1
    我是import进来的
  • index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
<?php
define('WEB_ROOT',dirname(__FILE__) . '/');
function sp_file_put_contents($file, $data) {
    $dir = dirname($file);
    if (is_writeable($dir)) {
        return file_put_contents($file, $data);
    } else {
        throw new writeFail(sprintf("写入文件%s,失败,目录不可写", $file), 503);
    }
}
class _HtmlTag{
    protected $taglist = "if|elseif|else|loop|for|while|=|:=|:e|:html|:";
    protected $string;
    protected $tpldir;
    public $findReg;
    function __construct($dir, $tpl) {
        $this->findReg = "/\{[ ]*($this->taglist)(\s*[^\{\}]+\s*)?\}/i";
        $this->tpldir = $dir;
        $this->tpl =$tpl;
    }
    function render($s) {
        $s = str_replace(['<--{', '}-->'], ['{','}'], $s);
        if (strpos($s, "{extends") !== false) {
            $s = $this->_extends($s);
        }
        $s = preg_replace('/[\n\r]+/s', "\n", $s);
        $s = preg_replace('/\r+/s', "", $s);
        $s = preg_replace('/\t+/s', "\t", $s);
        $this->string = str_replace(["\{", "\}"], ["\HKH1", "\HKH2"], $s);
        preg_match_all("/\{[ ]*(import)(\s*[^\{\}]+\s*)?\}/i", $s, $d);
        foreach($d[0] as $k => $v) {
            call_user_func_array(array($this, "_import"), array($v, $d[1][$k], $d[2][$k]));
        }
        static $m = array("=" => "_defaultEcho", ":=" =>" _xssEcho", ":e" => "_htmlencodeEcho", ":" => "_echo", ":html" => "_htmldecodeEcho");
        preg_match_all($this->findReg, $this->string, $d);
        foreach ($d[0] as $k => $v) {
            if (isset($m[$d[1][$k]])) {
                $medth = $m[$d[1][$k]];
            } else {
                $medth = '_' . $d[1][$k];
            }
            call_user_func_array(array($this, $medth), array($v, $d[1][$k], $d[2][$k]));
        }
    }
    function _extends($tpl) {
        preg_match("/\{\s*extends\s+([^\{\}]+?)\s*\}/i", $tpl, $d);
        list($search, $arg) = $d;
        if (stripos($arg, ".") !==0 ) {
            $arg = '../' . $arg;
        }
        //$file = $this->tpl->me->realTpl($arg . ".htm");
        $file = $arg . ".htm";
        $basetpl = file_get_contents($file);
        preg_match_all("/\{block\s+([^\{\}]+?)\s*\}(.+?)\{\/block\}/s", $tpl, $ds);
        foreach($ds[1] as $n => $name) {
            $basetpl = preg_replace("/\{block\s+name={$name}\s*\}/", $ds[2][$n], $basetpl);
        }
        $basetpl = preg_replace("/\{block\s+name=.+?\}/", ' ', $basetpl);
 
        return $basetpl;
    }
    function _import ($search, $tag, $arg) {
 
        $arg = trim($arg);
        if (stripos($arg, ".") !== 0) {
            $arg = '../' . $arg;
        }
        //$file = $this->tpl->me->realTpl($arg . ".htm");
        $file = $arg . ".htm";
        if (file_exists($file)) {
            $this->string = str_replace($search, file_get_contents($file), $this->string);
            return;
        } else {
            if (stripos($file, "$") !== false) {
            $this->string = str_replace($search, '<? include $this->tpl->display("' . $arg . '"); ?>', $this->string);
            return;
        }
        if (stripos($arg, '|') !== false) {
            list($func, $tmp) = explode("|", trim($arg));
            $kw = explode(', ', $tmp);
        } else {
            $func = trim($arg);
            $kw = array(); 
        }
        if (function_exists($func)) {
            $tpl_str = call_user_func_array($func, $kw);
            $this->string = str_replace($search, $tpl_str, $this->string);
            return;
        } else {
            $this->string = str_replace($search, $arg, $this->string);
            return;
        }
            system_error($this->tpldir . trim($arg) . ".htm不存在");
        }
    }
 
    function mytest(){
        return $this->string;
    }
 
 
    function _baseParse($s) {
        if (strpos($s, "{loadCss") !== false) {
            $s = $this->_loadCss($s);
        }
        if (strpos($s, "{loadJs") !== false) {
            $s = $this->_loadJs($s);
        }
        return $s;
    }
 
    function fileMd5($f) {
        if (stripos($f, "http://") !== false) {
            return md5($f);
        }
            return md5_file(WEB_ROOT . $f);
    }
 
    function _loadCss($tpl) {
        preg_match_all('/\{loadCss\s+([^\{\}]+)\s*\}/i' , $tpl , $match);
        $cdn = "";
        foreach($match[1] as $k => $css) {
            $cssstr = "";
            if (stripos($css, ',') !== false) {
                $filename = "";
                $md5 = "";
                $css_content = "";
                foreach(explode(',', $css) as $css) {
                    $filename .= basename($css, '.css') . ',';
                    $md5 = self::fileMd5($css);
                    $css_content .= file_get_contents(WEB_ROOT .$css);  
                }
                $md5 = md5($md5);
                $filename = dirname($css) . '/' . trim($filename, ',') . '.css';
                self::parseCss($css_content, $filename);
                $cssstr = "<link rel=\"stylesheet\" href=\"{$cdn}{$filename}?v={$md5}\">";
                } else {
                    $md5 = self::fileMd5($css);
                    if (stripos($css, ".min.") == false) {
                        $css = self::parseCss(file_get_contents(WEB_ROOT . $css), dirname($css) . '/' .basename($css, '.css') . '.min.css');
                    }
                    $cssstr .= "<link rel=\"stylesheet\" href=\"{$cdn}{$css}?v={$md5}\">";
            }
            $tpl = str_replace($match[0][$k], $cssstr, $tpl);
        }
        return $tpl;
    }   
 
 
    function _loadJs($tpl) {
 
        preg_match_all('/\{loadJs\s+([^\{\}]+?)\s*\}/i', $tpl, $match);
        //var_dump($match);die;
        include "jsmin.class.php";
        foreach($match[1] as $k => $js) {
            $jsstr = "";
            $cdn = "";
            if (stripos($js, ',') !== false) {
                $filename = "";
                $md5 = "";
                $js_content = "";
                foreach(explode(',', $js) as $j) {
                    $filename .= basename($j, '.js') . ',';
                    $md5 .= self::fileMd5($j);
                    if (stripos($j, '.min.') === false) {
                        $js_content .= JSMin::minify(file_get_contents(WEB_ROOT . $j));
                    } else {
                        $js_content .= file_get_contents(WEB_ROOT . $j);
                    }
                    $js_content .= ';';
                }
                $md5 = md5($md5);
 
                $filename = dirname($js) . '/' .trim($filename, ',') . '.js';
                self::parseJs($js_content, $filename);
                $jsstr = "<script src=\"{$cdn}{$filename}?v={$md5}\"></script>";
            } else {
                $md5 = self::fileMd5($js);
                if (stripos($js, ".min.") === false) {
                    $js = self::pareJs(JSMin::minify(file_get_contents(WEB_ROOT . $js)), dirname($js). '/' .basename($js, '.js') . '.min.js');
                }
                $jsstr = "<script src=\"{$cdn}{$js}?v={$mdt}\"></script>";
            }
            $tpl = str_replace($match[0][$k], $jsstr, $tpl);
        }
        return $tpl;
    }
 
    static function parseCss($css_content, $filenme) {
 
        $css_content = preg_replace("/[\r\n\t]/", '', $css_content);
        $css_content = preg_replace("/ +/", ' ', $css_content);
        // echo WEB_ROOT . $filenme;
 
        sp_file_put_contents(WEB_ROOT . $filenme, $css_content);
 
        return $filenme;
    }
 
    static function parseJs($js, $filenme) {
 
        sp_file_put_contents(WEB_ROOT . $filenme, $js);
 
        return $filenme;
 
    }
 
    function _loop($search, $tag, $arg) {
        list($attr, $arg) = $this->parseAttr($arg);
        $d = preg_split("/\s+/", trim($arg));
        if (count($d) == 3) {
            $data = $d[0];
            $k = $d[1];
            $v = $d[2];
            $s = "<? \n if(!empty($data)) {\n \t foreach(" . $data . " as {$k} => {$v}){";
        } elseif (count($d) == 2) {
            $data = $d[0];
            $v = $d[1];
            $s = "<? \n if(!empty($data)){\n \t foreach(". $datsa . " as {$v}) {";
        }
 
        if (isset($attr['counter'])) {
            $s = "\n\t\t <? if (!isset({$attr['counter']})) { {$attr['counter']} = 0;} ?> \n \t\t" . $s . "\n \t\t{$attr['counter']}++; \n ?>";
            } else {
            $s .= "?>";
        }
        $this->string = str_replace($search, $s, $this->string);
        $this->string = str_replace("{/loop}", "<? \t}\n}?>", $this->string);
    }
    function parseAttr($s) {
        $reg = '/([a-zA-Z0-9_])+\s*=\s*([a-zA-Z0-9_\$]+)/i';
        $arg = preg_replace($reg, '', $s);
        preg_match_all($reg, $s, $d);
        $arr = array();
        foreach($d[1] as $key => $value) {
            $arr[trim($value)] = trim($d[2][$key]);
        }
        return array($arr, $arg);
    }
    function _if ($search, $tag, $arg) {
        $replace[0] = "<? if($arg){?>";
        $replace[1] = "<? }?>";
        $this->string = str_replace(array($search, "{/$tag}"), $replace, $this->string);
    }
    function _else($search, $tag, $arg) {
        $this->string = str_replace("{else}", "\n" . '<? } else {?>', $this->string);
    }
    function _elseif($search, $tag, $arg) {
     $this->string = str_replace($search, "<? }elseif($arg){?>", $this->string);
    }
 
    function _for($search, $tag, $arg) {
      $s = trim(trim(preg_replace('/\s+/', ' ; ', $arg)), ';');
      $this->string = str_replace($search, "<? for($s){?>", $this->string);
      $this->string = str_replace("{/for}", "<? \t\n}?>", $this->string);
    }
    function _while($search, $tag, $arg) {
      $this->string = str_replace($search , "<? while($arg){ ?>", $this->string);
      $this->string = str_replace("{/while}", "<? \t\n} ?>", $this->string);
    }
    function _end() {
        //{$val}标签解析
        $this->string = preg_replace('/\\{(\\$\\w+.*)\\}/is', "<?=\\\\1;?>", $this->string);
        //{:$val()}标签解析
        $this->string = preg_replace('/\\{\\:\\s*(\\$?\\w+.*?)\\}/is', "<? =\\\\1;?>", $this->string);
        //将$val[arg]解析成$val['arg']
        $this->string = preg_replace('/\\$([_a-z]+\\w*)\\[([_a-z]+\\w*)\\]/is', "$\\\\1['\\\\2']", $this->string);
        //支持点号访问数组。如array['key']可以用array.key访问
        $this->string = preg_replace('/\\$([_a-z]+\\w*)\\.(\\$[_a-z]+\\w*)/is', "$\\\\1[\\\\2]", $this->string);
        $this->string = preg_replace('/\\$([_a-z]+\\w*)\\.([_a-z]+\\w*)/is', "$\\\\1['\\\\2']", $this->string);
        $this->sring = preg_replace(array("/<\?/", "/<\?php\s*=/"), array("<?php ", "<?php echo "), $this->string);
    }
    function result() {
        $this->_end();
        $this->string = $this->_baseParse($this->string);
        return str_replace(array("\\HKH1", "\\HKH2"), array("\\{", "\\}"), $this->string);
    }
}
$data = ['aa','bb','cc'];
$bb = 'aa';
$con = 11;
$tag = new _HtmlTag('./index', '');
$str = file_get_contents('base.htm');
$tag->render($str);
$html = $tag->result();
var_dump($html);
echo '</br>----------------------------</br>';
sp_file_put_contents("./index.html", $html);
include "index.html";
运行index.php查看结果
php模板引擎的原理与简单实例
 

php模板引擎的原理与简单实例
1
2
3
总结
 
我们来总结一下这个类的使用过程,先实例话template类,调用template::display()方法,其中会调用template::compiles()方法,这个方法会实例化_HhtmlTag类,调用_HtmlTag::render方法,在这个方法里,对传入的带字符串进行一个结构的完整化,包括调用_HtmlTag::_extends,_HtmlTag::_import,_HtmlTag::_loop,_HtmlTag::_if,_HtmlTag::elseif,_HtmlTag::else,_HtmlTag::_for,_HtmlTag::while接着调用_HtmlTag::result方法,里面调用_HtmlTag::_end方法对变量进行php原生化和_HtmlTag::_baseParse()引入css和js,最后返回解析好的完整模板字符串。