构建自己的PHP框架--构建模版引擎(3)
之前我们实现了最简单的echo
命令的模版替换,就是将{{ $name }}
这样一段内容替换成<?php echo $name ?>
。
现在我们来说下其他的命令,先来回顾下之前的定义
- 输出变量值
{{ }}
表达式的返回值将被自动传递给 PHP
的 htmlentities
函数进行处理,以防止 XSS
攻击。
Hello, {{ $name }}!
- 输出未转义的变量值
Hello, {!! $name !!}!
- If 表达式
通过 @if
、@elseif
、@else
和 @endif
指令可以创建 if
表达式。
@if (count($records) === 1)
I have one record!
@elseif (count($records) > 1)
I have multiple records!
@else
I don't have any records!
@endif
- 循环
@for ($i = 0; $i < 10; $i++)
The current value is {{ $i }}
@endfor
@foreach ($users as $user)
<p>This is user {{ $user->id }}</p>
@endforeach
@while (true)
<p>I'm looping forever.</p>
@endwhile
- 引入其他视图
@include('view.name', ['some' => 'data'])
要匹配这些定义,我们要写出相应的正则表达式,关于@
开头的命令直接拿了laravel
中的使用。
我们先在src
下创建view
文件夹,再创建Compiler
类文件。
我们将compiler的方式氛围两种,一种是@
开头的命令(Statements
),一种是输出(Echos
)。这两种的正则是不一样的。
首先定义变量compliers
,定义如下:
protected $compilers = [
'Statements',
'Echos',
];
然后按着见原来Controller
中render
方法的内容迁移到Complier
类中,按这两种依次匹配,代码如下:
public function compile($path = null)
{
$fileContent = file_get_contents($path);
$result = '';
foreach (token_get_all($fileContent) as $token) {
if (is_array($token)) {
list($id, $content) = $token;
if ($id == T_INLINE_HTML) {
foreach ($this->compilers as $type) {
$content = $this->{"compile{$type}"}($content);
}
}
$result .= $content;
} else {
$result .= $token;
}
}
$generatedFile = '../runtime/cache/' . md5($path);
file_put_contents($generatedFile, $result);
require_once $generatedFile;
}
protected function compileStatements($content)
{
return $content;
}
protected function compileEchos($content)
{
return preg_replace('/{{(.*)}}/', '<?php echo $1 ?>', $content);
}
其中的Statements
完全没有处理,Echos
则还是跟之前一样。
先来调整下Echos
中的处理,添加变量记录{{ }}
和{!! !!}
的名称
protected $echoCompilers = [
'RawEchos',
'EscapedEchos'
];
处理的时候可以添加一下存在的判断,默认值是null
,内容可以调整如下:
protected function compileEchos($content)
{
foreach ($this->echoCompilers as $type) {
$content = $this->{"compile{$type}"}($content);
}
return $content;
}
protected function compileEscapedEchos($content)
{
return preg_replace('/{{(.*)}}/', '<?php echo htmlentities(isset($1) ? $1 : null) ?>', $content);
}
protected function compileRawEchos($content)
{
return preg_replace('/{!!(.*)!!}/', '<?php echo isset($1) ? $1 : null ?>', $content);
}
EscapedEchos
和RawEchos
的区别在于,第一个会做html
转义。
我们再来看Statements
命令的处理,其原理也一样,匹配到相应的命令,如if
、foreach
,调用相应的方法做替换。
代码如下:
protected function compileStatements($content)
{
return preg_replace_callback(
'/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x', function ($match) {
return $this->compileStatement($match);
}, $content
);
}
protected function compileStatement($match)
{
if (strpos($match[1], '@') !== false) {
$match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1];
} elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) {
$match[0] = $this->$method(isset($match[3]) ? $match[3] : null);
}
return isset($match[3]) ? $match[0] : $match[0].$match[2];
}
protected function compileIf($expression)
{
return "<?php if{$expression}: ?>";
}
protected function compileElseif($expression)
{
return "<?php elseif{$expression}: ?>";
}
protected function compileElse($expression)
{
return "<?php else{$expression}: ?>";
}
protected function compileEndif($expression)
{
return '<?php endif; ?>';
}
protected function compileFor($expression)
{
return "<?php for{$expression}: ?>";
}
protected function compileEndfor($expression)
{
return '<?php endfor; ?>';
}
protected function compileForeach($expression)
{
return "<?php foreach{$expression}: ?>";
}
protected function compileEndforeach($expression)
{
return '<?php endforeach; ?>';
}
protected function compileWhile($expression)
{
return "<?php while{$expression}: ?>";
}
protected function compileEndwhile($expression)
{
return '<?php endwhile; ?>';
}
protected function compileContinue($expression)
{
return '<?php continue; ?>';
}
protected function compileBreak($expression)
{
return '<?php break; ?>';
}
其中的include
实现比较麻烦,就没有做,留给大家思考啦。
然后,我们再考虑一下,不可能每次都去操作文件重新生成,我应该要判断文件改变,如果没改变直接使用缓存就可以了。
调整代码如下:
public function isExpired($path)
{
$compiled = $this->getCompiledPath($path);
if (!file_exists($compiled)) {
return true;
}
return filemtime($path) >= filemtime($compiled);
}
protected function getCompiledPath($path)
{
return '../runtime/cache/' . md5($path);
}
public function compile($file = null, $params = [])
{
$path = '../views/' . $file . '.sf';
extract($params);
if (!$this->isExpired($path)) {
$compiled = $this->getCompiledPath($path);
require_once $compiled;
return;
}
$fileContent = file_get_contents($path);
$result = '';
foreach (token_get_all($fileContent) as $token) {
if (is_array($token)) {
list($id, $content) = $token;
if ($id == T_INLINE_HTML) {
foreach ($this->compilers as $type) {
$content = $this->{"compile{$type}"}($content);
}
}
$result .= $content;
} else {
$result .= $token;
}
}
$compiled = $this->getCompiledPath($path);
file_put_contents($compiled, $result);
require_once $compiled;
}
这个系列的博客到这里就暂时告一段落了~
项目内容和博客内容也都会放到Github上,欢迎大家提建议。
code:https://github.com/CraryPrimitiveMan/simple-framework/tree/1.2
blog project:https://github.com/CraryPrimitiveMan/create-your-own-php-framework