简单的PHP模板......这可以在没有`eval`的情况下工作吗?

时间:2010-10-14 04:17:46

标签: php templates eval heredoc

更新 - 感谢所有回复。这个问题变得很混乱,所以如果有人感兴趣,我就开始sequel


我正在为一位朋友拼凑一个快速脚本,偶然发现了一种在PHP中进行模板操作的简单方法。

基本上,我们的想法是将html文档解析为heredoc字符串,因此PHP中的变量将被扩展。

passthrough函数允许在字符串中进行表达式求值和函数以及静态方法调用:

function passthrough($s){return $s;}
$_="passthrough";

在heredoc字符串中解析文档的代码非常简单:

$t=file_get_contents('my_template.html');
eval("\$r=<<<_END_OF_FILE_\n$t\_END_OF_FILE_;\n");
echo $r;

唯一的问题是,它使用eval

问题

  • 有没有人能想到一种方法来做这种模板而不使用eval,但是没有添加解析器或大量的正则表达式疯狂?

  • 如果没有编写完整的解析器,有什么建议可以逃避不属于PHP变量的流浪美元符号?流浪的美元符号问题是否会使这种方法不适合“严重”使用?


这是一些模板化的HTML代码示例。

<script>var _lang = {$_(json_encode($lang))};</script>
<script src='/blah.js'></script>
<link href='/blah.css' type='text/css' rel='stylesheet'>

<form class="inquiry" method="post" action="process.php" onsubmit="return validate(this)">

  <div class="filter">
    <h2> 
      {$lang['T_FILTER_TITLE']}
    </h2>
    <a href='#{$lang['T_FILTER_ALL']}' onclick='applyFilter();'>
      {$lang['T_FILTER_ALL']}
    </a>
    {$filter_html}
  </div>

  <table class="inventory" id="inventory_table">
    {$table_rows}
    <tr class="static"><th colspan="{$_($cols+1)}">
      {$lang['T_FORM_HELP']}
    </th></tr>
    {$form_fields}
    <tr class="static">
      <td id="validation" class="send" colspan="{$cols}">&nbsp;</td>
      <td colspan="1" class="send"><input type="submit" value="{$lang['T_SEND']}" /></td>
    </tr>
  </table>

</form>

为什么要使用模板?


有人讨论过在PHP中是否需要创建一个模板层,不可否认,这已经非常适合模板化。

模板的一些快速原因很有用:

  • 您可以控制

    如果您在将文件转到解释器之前对其进行预处理,则可以对其进行更多控制。你可以注入东西,锁定权限,抓取恶意php / javascript,缓存它,通过xsl模板运行它,等等。

  • 良好的MVC设计

    模板促进了视图与模型和控制器的分离。

    当您在视图中跳入和跳出<?php ?>标记时,很容易变得懒惰并执行一些数据库查询或执行其他一些服务器操作。使用上面这样的方法,每个'block'只能使用一个语句(没有分号),因此陷入陷阱要困难得多。 <?= ... ?>有相同的好处,但是......

  • 并非始终启用短标记

    ...我们希望我们的应用程序可以运行各种配置。

当我最初一起破解一个概念时,它开始是一个php文件。但是在它增长之前,我不高兴,除非所有的php文件在开头只有一个<?php,最后只有一个?>,最好所有类都是类,除了控制器,设置,图像之类的东西服务器等。

我根本不想在我的视图中使用PHP,因为当看到类似这样的东西时,设计师会因为Dreamweaver或其他任何事情而感到困惑:

<a href="<?php $img="$img_server/$row['pic'].png"; echo $img; ?>">
  <img src="<?php echo $img; ?>" /></a>

这对程序员来说很难看。普通的平面设计师不会靠近它。这样的事情更容易应对:

<a href="{$img}"><img src="{$img}" /></a>

程序员将他讨厌的代码保留在html中,现在设计师可以完成他的设计魔术。耶!

快速更新

考虑到每个人的建议,我认为预处理文件是要走的路,中间文件应该尽可能接近正常的“php模板”,模板是语法糖。当我玩它时,Eval现在仍然存在。 heredoc的东西改变了它的作用。我稍后会写更多,并尝试回答一些答案,但现在......

<?php



class HereTemplate {

  static $loops;

  public function __construct () {
    $loops=array();
  }

  public function passthrough ($v) { return $v; }

  public function parse_markup ($markup, $no_escape=null, $vars=array()) {
    extract($vars);
    $eot='_EOT_'.rand(1,999999).'_EOT_';
    $do='passthrough';
    if (!$no_escape) $markup=preg_replace(
      array(
        '#{?{each.*(\$\w*).*(\$\w*).*(\$\w*).*}}?#', 
        '#{?{each.*(\$\w*).*(\$\w*).*}}?#', 
        '#{?{each}}?#',
        '#{{#', '#}}#',
        '#{_#', '#_}#',
        ),
      array(
        "<?php foreach (\\1 as \\2=>\\3) { ?>", 
        "<?php foreach (\\1 as \\2) { ?>", 
        "<?php } ?>",
        "<?php echo <<<$eot\n{\$this->passthrough(", ")}\n$eot\n ?>",
        "<?php ", " ?>",
        ), 
      $markup);
    ob_start(); 
    eval(" ?>$markup<?php ");
    echo $markup;
    return ob_get_clean();
  }

  public function parse_file ($file) {
    // include $file;
    return $this->parse_markup(file_get_contents($file));
  }

}


// test stuff


$ht = new HereTemplate();
echo $ht->parse_file($argv[1]);


?>

...

<html>

{{each $_SERVER $key $value}

<div id="{{$key}}">

{{!print_r($value)}}

</div>

{each}}



</html>

8 个答案:

答案 0 :(得分:24)

PHP最初本身就是一种模板语言(即允许您在HTML中嵌入代码的简单方法)。

正如您从自己的示例中看到的那样,它过于复杂,无法证明在大多数情况下以这种方式使用它是正确的,所以良好的做法从此转而使用它作为传统语言,并且只是突破{ {1}}标记尽可能少。

麻烦的是人们仍然想要一种模板语言,所以像Smarty这样的平台被发明了。但是如果你现在看看它们,Smarty支持像自己的变量和foreach循环这样的东西......不久之后,Smarty模板开始出现与PHP模板一样的问题;你也可以在最初使用原生PHP。

我在这里要说的是,简单的模板语言的理想实际上并不容易。实际上几乎不可能使它既不够简单,也不会吓跑设计师,同时给予它足够的灵活性来实际完成你需要它做的事情。

答案 1 :(得分:9)

我会做一些愚蠢的事情并建议一些根本不需要模板引擎的东西,每个变量/调用只需要最多5个字符而不是你所拥有的那些 - 用{$foo}替换<?=$foo?>然后你可以使用include来满足你所有的模板需求

如果您需要的是变量替换,尽管这是一个模板函数我实际上使用:

function fillTemplate($tplName,$tplVars){
  $tpl=file_get_contents("tplDir/".$tplName);
  foreach($tplVars as $k=>$v){
    $tpl = preg_replace('/{'.preg_quote($k).'}/',$v,$tpl);
  }
  return $tpl;
}

如果你想能够调用函数或有循环,基本上没有办法调用eval而不是预处理。

答案 2 :(得分:8)

如果您不习惯使用Twig之类的大型模板引擎(我诚挚地推荐),您只需很少的代码就可以获得良好的效果。

所有模板引擎共享的基本思想是使用友好,易于理解的语法编译模板,以快速和可缓存的PHP代码。通常,他们会通过解析您的源代码然后编译它来实现这一目标。但即使你不想使用复杂的东西,你也可以使用正则表达式获得好的结果。

所以,基本想法:

function renderTemplate($templateName, $templateVars) {
    $templateLocation = 'tpl/'      . $templateName . '.php';
    $cacheLocation    = 'tplCache/' . $templateName . '.php';
    if (!file_exists($cacheLocation) || filemtime($cacheLocation) < filemtime($templateLocation)) {
        // compile template and save to cache location
    }

    // extract template variables ($templateVars['a'] => $a)
    extract($templateVars);

    // run template
    include 'tplCache/' . $templateName . '.php';
}

所以基本上我们首先编译模板然后执行它。仅当缓存的模板尚不存在或模板的版本高于缓存中的模板时才进行编译。

所以,我们来谈谈编译。我们将定义两种语法:输出和控制结构。默认情况下,输出始终是转义的。如果您不想逃避它,您必须将其标记为“安全”。这提供了额外的安全性所以,这是我们语法的一个例子:

{% foreach ($posts as $post): }
    <h1>{ $post->name }</h1>
    <p>{ $post->body }</p>
    {!! $post->link }
{% endforeach; }

所以,你使用{ something }来逃避并回应某些东西。您可以使用{!! something}直接回显某些内容,而无需转义它。并且使用{% command }执行一些PHP代码而不回显它(例如用于控制结构)。

所以,这是编译代码:

$code = file_get_contents($templateLocation);

$code = preg_replace('~\{\s*(.+?)\s*\}~', '<?php echo htmlspecialchars($1, ENT_QUOTES) ?>', $code);
$code = preg_replace('~\{!!\s*(.+?)\s*\}~', '<?php echo $1 ?>', $code);
$code = preg_replace('~\{%\s*(.+?)\s*\}~', '<?php $1 ?>', $code);

file_put_contents($cacheLocation, $code);

就是这样。您必须注意,这比真正的模板引擎更容易出错。但它适用于大多数情况。此外请注意,这允许模板的编写者执行任意代码。这既是专业人士,也是骗子。

所以,这是整个代码:

function renderTemplate($templateName, $templateVars) {
    $templateLocation = 'tpl/'      . $templateName . '.php';
    $cacheLocation    = 'tplCache/' . $templateName . '.php';
    if (!file_exists($cacheLocation) || filemtime($cacheLocation) < filemtime($templateLocation)) {
        $code = file_get_contents($templateLocation);

        $code = preg_replace('~\{\s*(.+?)\s*\}~', '<?php echo htmlspecialchars($1, ENT_QUOTES) ?>', $code);
        $code = preg_replace('~\{!!\s*(.+?)\s*\}~', '<?php echo $1 ?>', $code);
        $code = preg_replace('~\{%\s*(.+?)\s*\}~', '<?php $1 ?>', $code);

        file_put_contents($cacheLocation, $code);
    }

    // extract template variables ($templateVars['a'] => $a)
    extract($templateVars, EXTR_SKIP);

    // run template
    include 'tplCache/' . $templateName . '.php';
}

我没有测试过上面的代码;)这只是基本的想法。

答案 3 :(得分:3)

没有最终解决方案。每个人都有利弊。但是你已经完成了你想要的东西。这似乎是一个非常明智的方向。所以我建议你找到最有效的方法来实现它。

你基本上只需要将你的文件包含在一些heredoc语法糖中。在每个文件的开头:

<?=<<<EOF

在每个模板文件的末尾:

EOF;
?>

成就奖。但显然这会混淆大多数语法高亮引擎。我可以修复我的文本编辑器,它是开源的。但Dreamweaver是另一回事。因此,唯一有用的选择是使用一个小的预编译器脚本,该脚本可以使用原始$ varnames-HTML和Heredoc包含的模板在模板之间进行转换。这是一个非常基本的正则表达式和文件重写方法:

#!/usr/bin/php -Cq
<?php
foreach (glob("*.tpl") as $fn) {
    $file = file_get_contents($fn);
    if (preg_match("/<\?.+<<</m")) {  // remove
        $file = preg_replace("/<\?(=|php\s+print)\s*<<<\s*EOF\s*|\s+EOF;\s*\?>\s*/m", "", $file);
    }
    else {   // add heredoc wrapper
        $file = "<?php print <<<EOF\n" . trim($file) . "\nEOF;\n?>";
    }
    file_put_contents($fn, $file);
}
?>

这是给定的 - 某个地方 需要带有少量if-else逻辑的模板。因此,对于一致处理,您应该使所有模板都表现为正确的PHP,而无需特殊的eval / regex处理包装。这使您可以轻松地在heredoc模板之间切换,但也有一些具有正常<?php print输出。适当地混合和匹配,设计人员可以处理大多数文件,但避免了一些复杂的情况。例如,我经常使用我的模板:

include(template("index"));   // works for heredoc & normal php templ

没有额外的处理程序,适用于两种常见的模板类型(原始php和smartyish html文件)。唯一的缺点是偶尔使用所述转换器脚本。

为了安全起见,我还会在每个模板的顶部添加extract(array_map("htmlspecialchars",get_defined_vars()));

无论如何,我不得不说你的passthrough方法非常聪明。我会调用heredoc别名$php,因此$_仍可用于gettext。

<a href="calc.html">{$php(1+5+7*3)}</a> is more readable than Smarty

我想我会自己采用这个技巧。

<div>{$php(include(template($ifelse ? "if.tpl" : "else.tpl")))}</div>

伸展它有点,但似乎毕竟可能在heredoc模板中有简单的逻辑。可能导致模板文件损害,但有助于实施最简单的模板逻辑。

  

Offtopic:如果三个<<<heredoc&EOF;语法行仍显示太脏,那么最佳no-eval选项是使用基于正则表达式的解析器。我不同意常见的神话,即它比原生PHP慢。事实上,我认为PHP tokenizer和解析器落后于PCRE。特别是如果完全关于插值变量。只是后者不是APC / Zend缓存,你可以自己在那里。

答案 4 :(得分:2)

就个人而言,我不会触及任何模板系统,忘记转义变量会产生远程执行代码漏洞。

答案 5 :(得分:0)

就我个人而言,我正在使用此模板引擎:http://articles.sitepoint.com/article/beyond-template-engine/5

我非常喜欢它,特别是因为它很简单。它有点类似于你最近的化身,但恕我直言比使用heredoc更好的方法,并在PHP之上放置另一层解析。也没有eval(),但输出缓冲和范围模板变量。使用方式如下:

<?php   
require_once('template.php');   

// Create a template object for the outer template and set its variables.     
$tpl = new Template('./templates/');   
$tpl->set('title', 'User List');   

// Create a template object for the inner template and set its variables.
// The fetch_user_list() function simply returns an array of users.
$body = new Template('./templates/');   
$body->set('user_list', fetch_user_list());   

// Set the fetched template of the inner template to the 'body' variable
// in the outer template.
$tpl->set('body', $body->fetch('user_list.tpl.php'));   

// Echo the results.
echo $tpl->fetch('index.tpl.php');   
?>

outter模板如下所示:

<html>
  <head>
    <title><?=$title;?></title>
  </head>
  <body>
    <h2><?=$title;?></h2>
        <?=$body;?>
  </body>
</html>

和内部的(进入外部模板的$body变量)如下:

<table>
   <tr>
       <th>Id</th>
       <th>Name</th>
       <th>Email</th>
       <th>Banned</th>
   </tr>
<? foreach($user_list as $user): ?>
   <tr>
       <td align="center"><?=$user['id'];?></td>
       <td><?=$user['name'];?></td>
       <td><a href="mailto:<?=$user['email'];?>"><?=$user['email'];?></a></td>
       <td align="center"><?=($user['banned'] ? 'X' : '&nbsp;');?></td>
   </tr>
<? endforeach; ?>
</table>

如果您不喜欢/不能使用短标签,请将其替换为回声。这就像你可以得到的那样接近简单,同时仍然拥有你需要的所有功能恕我直言。

答案 6 :(得分:0)

使用函数的死简单模板:

<?php

function template($color) {
        $template = <<< ENDTEMPLATE
The colors I like are {$color} and purple.
ENDTEMPLATE;

        return $template . "\n";
}

$color = 'blue';
echo template($color);

$color = 'turquoise';
echo template($color);

输出:

The colors I like are blue and purple.
The colors I like are turquoise and purple.

没什么特别的,但它确实可以使用没有扩展名的标准PHP。此外,使用函数来封装模板应该有助于正确的MVC分离。另外(这就是我今天编码所需要的)我可以将填好的模板保存起来输出到文件中(稍后在我的程序中)。

答案 7 :(得分:0)

这是胡子的最小实现,只是替换变量。

// Example:
//   miniMustache(
//      "{{documentName }} - pag {{ page.current }} / {{ page.total }}",
//      array(
//         'documentName' => 'YourCompany Homepage', 
//         'page' => array('current' => 1, 'total' => 10)
//      )
//    )
//    
// Render: "YourCompany Homepage - pag 1 / 10"

    function miniMustache($tmpl, $vars){
        return preg_replace_callback( '/\{\{([A-z0-9_\.\s]+)\}\}/',
            function ($matches) use ($vars) {
                //Remove white spaces and split by "."
                $var = explode('.',preg_replace('/[\s]/', '', $matches[1]));
                $value = $vars;
                foreach($var as $el){
                    $value = $value[$el];
                }
                return $value;
            }, 
            $tmpl);
    }

在某些情况下,绰绰有余。如果您需要全功率:https://github.com/bobthecow/mustache.php