PHP 造轮子 之 模板引擎

本文详细介绍了一款基于PHP的MVC框架中的模板引擎实现,包括变量解析、表达式处理、循环与条件语句编译等核心功能。通过具体语法示例,如变量输出、点语法、函数调用及模板继承等,展示了模板引擎的灵活性和强大功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在 造 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 语法 和 该模板语法 都注释掉。

 

后续模板引擎的更新我会发布在码云上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值