最近在 造 PHP MVC 框架的轮子,已经做到模板类了,发现已经好久没发博客了,所以就发布一个模板引擎的教程吧,模板引擎的原理差不多就是这样的,模板布局、子模板、以及模板缓存功能还没开发,如果有兴趣可以去看下 thinkphp5.1 的模板类,也就 1000 行代码而已。
<?php
class Template
{
private static $content = null;
private static $app_path = null;
private static $public_path = null;
public static function compile($fetch, $assign)
{
define('DS', DIRECTORY_SEPARATOR);
self::$app_path = ''; // mvc 框架中的 app 目录,用来保存控制器目录、视图目录、模型目录 的目录。
self::$public_path = ''; // public 目录
$config_path = ''; // 模板设置文件目录
$temp_php_path = ''; // 生成 php 编译文件的目录
$temp_html_path = ''; // 生成 html 静态文件的目录
if (is_file($config_path) !== true) {
//如果 模板设置文件不存在则终止
exit();
}
$config = require_once($config_path); //导入模板设置文件
if (array_key_exists('view_suffix', $config) !== true) {
//模板后缀设置不存在则终止
exit();
}
$fetch = preg_replace('/\//', DS, $fetch); //把 / 符号转成系统默认的目录分隔符
if (is_file(self::$app_path . $fetch . '.' . $config['view_suffix']) !== true) {
//指定的模板文件不存在则终止
exit();
}
//读取模板文件的内容到内存
self::$content .= file_get_contents(self::$app_path . $fetch . '.' . $config['view_suffix']);
//编译 注释
self::parseComment();
if (array_key_exists('replace_string', $config) === true && is_array($config['replace_string']) === true && count($config['replace_string']) > 0) {
//编译 指定的字符串
self::parseReplace($config['replace_string']);
}
//编译 需要导入的其它模板文件
self::parseInclude($config['view_suffix']);
//编译 需要导入的 js 文件
self::parseJs();
//编译 需要导入的 css 文件
self::parseCss();
//编译 。 符号
self::parseArrVar();
//编译 默认值
self::parseDefault();
//编译 函数
self::parseFun();
//编译 Foreach
self::parseForeach();
//编译 For
self::parseFor();
//编译 If
self::parseIf();
//编译 比较符
self::parseCompare();
//编译 表达式
self::parseExpression();
// 生成临时文件的名字
$temp_name = md5(uniqid(mt_rand(), true));
// 生成 编译的php文件的名字
$parFile = $temp_php_path . DS . $temp_name . '.php';
if (file_put_contents($parFile, self::$content) === false) {
//把内存中已编译好的内容写入这个文件,如果失败则终止。
exit('编译文件生成失败!');
}
//注入变量 和 导入 上面 编译好的php文件。
self::intoVar($assign, $parFile);
//把缓冲区的值保存在一个静态html文件中。
file_put_contents($temp_html_path . DS . $temp_name . '.html', ob_get_contents());
//清除缓存区内容
ob_end_clean();
//导入上面的html静态文件。
include_once($temp_html_path . DS . $temp_name . '.html');
}
private static function parseInclude($suffix)
{
$pattern = '/\{\{include\s+file="(\S+?\/)(\S+)"\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace_callback($pattern, function ($matches) use ($suffix) {
$path = preg_replace('/\//', DS, $matches[1] . 'view/' . $matches[2]);
$file = self::$app_path . DS . $path . '.' . $suffix;
if (is_file($file) !== true) {
exit('模板文件不存在');
}
return '<?php include(\'' . $file . '\');?>';
}, self::$content);
}
}
private static function parseReplace($replace)
{
foreach ($replace as $key => $value) {;
self::$content = str_replace($key, $value, self::$content);
}
}
private static function parseJs()
{
$pattern = '/\{\{js\s+src="([\S]+)"\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace_callback($pattern, function ($matches) {
$file = self::$public_path . DS . str_replace('/', DS, $matches[1]);
if (is_file($file) !== true) {
exit();
}
return '<script type="text/javascript" src="' . $matches[1] . '"></script>';
}, self::$content);
}
}
private static function parseCss()
{
$pattern = '/\{\{css\s+href="([\S]+)"\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace_callback($pattern, function ($matches) {
$file = self::$public_path . DS . str_replace('/', DS, $matches[1]);
if (is_file($file) !== true) {
exit();
}
return '<link type="text/css" rel="stylesheet" href="' . $matches[1] . '"/>';
}, self::$content);
}
}
private static function parseCompare()
{
$charArr = [
'eq' => '==',
'neq' => '!==',
'gt' => '>',
'egt' => '>=',
'lt' => '<',
'elt' => '<=',
'heq' => '===',
'nheq' => '!==',
];
$char = 'eq|neq|gt|egt|lt|elt|heq|nheq';
$pattern = '/\{\{(' . $char . ')\s+name="(\w+)"\s+value="([0-9]+)"\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace_callback($pattern, function ($matches) use ($charArr) {
if (array_key_exists($matches[1], $charArr) === true) {
return preg_replace('/\{\{' . $matches[1] . '\s+name="(\w+)"\s+value="([0-9]+)"\}\}/', '<?php if ($$1 ' . $charArr[$matches[1]] . ' $2 ){?>', $matches[0]);
}
}, self::$content);
}
self::$content = preg_replace('/\{\{\/(' . $char . ')\}\}/', '<?php }?>', self::$content);
}
private static function parseExpression()
{
$pattern = '/\{\{(.+?)\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace($pattern, '<?php echo htmlentities($1);?>', self::$content);
}
}
private static function parseArrVar()
{
$pattern = '/\{\{.*\$\w+\.\w+.*\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace_callback($pattern, function ($matches) {
$result = preg_replace_callback('/(\$\w+)\.([\w+\.]+)/', function ($matches) {
$explode = explode('.', $matches[2]);
$result = '';
foreach ($explode as $value) {
$result .= '[\'' . $value . '\']';
}
return $matches[1] . $result;
}, $matches[0]);
return $result;
}, self::$content);
}
}
private static function parseFun()
{
$pattern = '/\{\{(.+?)\|(.+)\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace($pattern, '<?php echo self::handleFun($1,\'$2\');?>', self::$content);
}
}
private static function parseDefault()
{
$pattern = '/\{\{(\s*\$.+?)\|default="(\S+)"(.*)\}\}/';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace($pattern, '{{isset($1) === true ? $1 : \'$2\' $3}}', self::$content);
}
}
private static function parseIf()
{
//开头if模式
$patternIf = '/\{\{if(.+?)\}\}/';
//结尾if模式
$patternEnd = '/\{\{\/if\}\}/';
//else if模式
$patternElseIf = '/\{\{else\s*if(.+?)\}\}/';
//else模式
$patternElse = '/\{\{else\}\}/';
//判断if是否存在
if (preg_match($patternIf, self::$content) === 1) {
//判断是否有if结尾
if (preg_match($patternEnd, self::$content) === 1) {
//替换开头IF
self::$content = preg_replace($patternIf, '<?php if($1) {?>', self::$content);
//替换结尾IF
self::$content = preg_replace($patternEnd, '<?php }?>', self::$content);
//判断是否有else If
if (preg_match($patternElseIf, self::$content) === 1) {
//替换else If
self::$content = preg_replace($patternElseIf, '<?php }else if($1){ ?>', self::$content);
}
//判断是否有else
if (preg_match($patternElse, self::$content) === 1) {
//替换else
self::$content = preg_replace($patternElse, '<?php }else{ ?>', self::$content);
}
} else {
exit('ERROR:语句没有关闭!');
}
}
}
private static function parseForeach()
{
$pattern = '/\{\{foreach\s+\$(\w+)\s*as\s*\$(\w+)\s*=>\s*\$(\w+)\}\}/';
$patternEnd = '/\{\{\/foreach\}\}/';
//判断是否存在
if (preg_match($pattern, self::$content) === 1) {
//判断结束标志
if (preg_match($patternEnd, self::$content) === 1) {
//替换开头
self::$content = preg_replace($pattern, '<?php foreach($$1 as $$2 => $$3){?>', self::$content);
//替换结束
self::$content = preg_replace($patternEnd, '<?php }?>', self::$content);
//替换值
} else {
exit('ERROR:foreach语句没有关闭');
}
}
}
private static function parseFor()
{
$pattern = '/\{\{for\s+\$(\w+)\s+in\s+\$(\w+)\}\}/';
$patternEnd = '/\{\{\/for\}\}/';
if (preg_match($pattern, self::$content) === 1) {
//判断结束标志
if (preg_match($patternEnd, self::$content) === 1) {
//替换开头
self::$content = preg_replace($pattern, '<?php foreach($$2 as $$1){?>', self::$content);
//替换结束
self::$content = preg_replace($patternEnd, '<?php }?>', self::$content);
} else {
exit('ERROR:for语句没有关闭');
}
}
}
private static function parseComment()
{
$pattern = '/\{%.*?%\}/s';
if (preg_match($pattern, self::$content) === 1) {
self::$content = preg_replace($pattern, '', self::$content);
}
}
private static function intoVar($assign, $parFile)
{
//生成指定的变量。
extract($assign);
//导入生成的php文件。
include_once($parFile);
}
private static function handleFun($target, $method)
{
$allowMethod = ['md5']; //允许运行的方法
$methodArr = explode('|', $method);
$raw = false;
foreach ($methodArr as $value) {
$explode = explode('=', $value);
if (in_array($explode[0], $allowMethod) !== true) {
exit();
}
if ($explode[0] === 'raw') {
$raw = true;
} else {
if (method_exists(Template::class, $explode[0]) !== true) {
exit('方法不存在' . $explode[0]);
}
if (count($explode) === 1) {
$target = call_user_func_array('self::' . $explode[0], [$target]);
} else if (count($explode) === 2) {
$target = call_user_func_array('self::' . $explode[0], [$target, $explode[1]]);
}
}
}
if ($raw === true) {
return $target;
} else {
return htmlentities($target);
}
}
private static function md5($str)
{
return md5($str);
}
}
使用教程
该模板用的是 mustache 语法。
1、变量的解析
{{$test}}
会编译成
<?php echo htmlentities($test);?>
2、表达式
{{$test === 1 ? 2 : 3}}
会编译成
<?php echo htmlentities($test=== 1 ? 2 : 3);?>
3、默认值
{{$test|default="test"}}
会编译成
<?php echo htmlentities(isset($test) === true ? $test : 'test' );?>
4、点语法
{{$value.name}}
会编译成
<?php echo htmlentities($value['name']);?>
支持多个点
{{$value.name.name.name}}
会编译成
<?php echo htmlentities($value['name']['name']['name']);?>
5、函数语法
支持运行多次函数,用 | 作分隔符
{{$test|md5|md5}}
比如把 $test 这个变量作为参数运行2次 md5 函数
会编译成
<?php echo self::handleFun($test,'md5|md5');?>
支持嵌套默认值语法
{{$test|default="test"|md5|md5|md5}}
会编译成
<?php echo self::handleFun(isset($test) === true ? $test: 'test' ,'md5|md5|md5');?>
6、IF 语法
{{if $test.name === 'test'}}
a
{{else}}
b
{{/if}}
会编译成
<?php if( $test['name'] === 'a') {?>
a
<?php }else{ ?>
b
<?php }?>
加 eles if
{{if $test.name === 'a'}}
a
{{else if $test.name === 'b'}}
b
{{else}}
c
{{/if}}
会编译成
<?php if( $test['name'] === 'a') {?>
a
<?php }else if( $test['name'] === 'b'){ ?>
b
<?php }else{ ?>
c
<?php }?>
7、For in 语法
{{for $test in $name}}
{{$name}}
{{/for}}
会编译成
<?php foreach($name as $test){?>
<?php echo htmlentities($name);?>
<?php }?>
8、 Foreach 语法
{{foreach $user as $key => $value}}
{{$value}}
{{/foreach}}
会编译成
<?php foreach($test as $key => $value){?>
<?php echo htmlentities($value);?>
<?php }?>
9、比较语法
{{eq name="id" value="0"}}
$id 等于 0
{{/eq}}
会编译成
<?php if ( $id == 0 ){?>
id 等于 0
<?php }?>
eq 是等于 、heq 是恒等于、nheq 是恒不等于、neq 是不等于、gt 是大于、 egt是 大于等于、lt 是小于、elt 是小于等于 。
10、引入其它模板文件
{{include file="index/login/header"}}
会编译成
<?php include('C:\Users\Administrator\Desktop\sphp\sphp\..\app\index\view\login\header.html');?>
11、引入 javascript 文件
{{js src="/static/javascript/Sue.js"}}
会编译成
<script type="text/javascript" src="/static/javascript/Sue.js"></script>
12、引入 css 文件
{{css href="__CSS__/test.css"}}
会编译成
<link type="text/css" rel="stylesheet" href="/static/css/test.css"/>
这里的 __CSS__ 用了全局替换。
13、全局替换
{{css href="__CSS__/test.css"}}
会编译成
<link type="text/css" rel="stylesheet" href="/static/css/test.css"/>
__CSS__ 替换了 /static/css 。
14、注释
注释用的语法 是 {% 注释内容 %}
该语法能把 html 语法 和 该模板语法 都注释掉。
后续模板引擎的更新我会发布在码云上。