【问题标题】:Dirt-simple PHP templates... can this work without `eval`?简单的 PHP 模板...没有 `eval` 可以工作吗?
【发布时间】:2011-04-25 05:01:18
【问题描述】:

更新-感谢所有回复。这个问题有点乱,所以如果有人感兴趣,我就发起了sequel


我正在为朋友编写一个快速脚本,偶然发现了一种在 PHP 中进行模板的非常简单的方法。

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

传递函数允许在字符串中进行表达式评估以及函数和静态方法调用:

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 已经非常擅长模板化。

模板有用的一些简单原因:

  • 你可以控制它

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

  • 良好的 MVC 设计

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

    当您在视图中进出&lt;?php ?&gt; 标记时,很容易变得懒惰并执行一些数据库查询或执行一些其他服务器操作。使用像上面这样的方法,每个“块”只能使用一个语句(没有分号),所以要陷入那个陷阱要困难得多。 &lt;?= ... ?&gt; 有几乎相同的好处,但是...

  • 并非总是启用短标签

    ...我们希望我们的应用在各种配置下运行。

当我最初将一个概念组合在一起时,它一开始是一个 php 文件。但在它增长之前,我并不高兴,除非所有 php 文件开头只有一个 &lt;?php,最后一个 ?&gt;,最好是除了控制器、设置、图像服务器等之外的所有类。

我根本不希望在我的观点中出现太多 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>

【问题讨论】:

  • @alex - 左右打开和关闭 PHP 标签正是我想要避免的......
  • @no:如果你不介意,你有什么理由避免它?为了更好看?对设计师来说更容易?干杯。
  • 这个问题表明糟糕的设计会导致问题。 PHP 是完美的模板系统,无需使用其他任何东西。
  • @Col。 Shrapnel: 是的,你在讲道理方面做得不太好……我更喜欢你只是看着,除非你能比以前更好地传达你的观点。

标签: php templates eval heredoc


【解决方案1】:

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

正如您从您自己的示例中看到的那样,在大多数情况下以这种方式使用它太复杂了,因此良好的实践从这种方式转变为更多地将其用作传统语言,并且只打破了@ 987654321@标签越少越好。

问题在于人们仍然想要一种模板语言,因此发明了像 Smarty 这样的平台。但是如果你现在看它们,Smarty 支持诸如它自己的变量和 foreach 循环之类的东西……不久之后,Smarty 模板开始出现与 PHP 模板过去相同的问题;你还不如一开始就使用原生 PHP。

我在这里想说的是,简单模板语言的理想实际上并不那么容易正确。要让它既简单又不吓跑设计师,同时又赋予它足够的灵活性来实际做你需要它做的事情,这几乎是不可能的。

【讨论】:

  • +1 劝阻人们不要为模板语言编写模板语言!
  • 我认为模板中的foreach 循环没什么不好。构建模板需要循环和条件。
  • @nikic - 我对 foreach 或模板语言中的任何其他编程方面没有任何问题;你是对的,它们是必要的(尽管经常被过度使用)。我的意思是回答关于 PHP 标记对于被编程结构吓跑的设计师来说太复杂的问题,通过表明所有模板语言都会受到这种影响,如果它们足够强大以实际有用的话。设计师将不得不习惯在他的模板中使用编程代码,在这种情况下,他最好还是坚持使用普通的 PHP。
【解决方案2】:

如果您不习惯使用像 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';
}

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

【讨论】:

  • 这也是我的方法。它有几个好处:1)它避免了 eval(),2)它避免了在每个页面加载时解析模板的开销,以及 3)Op 代码缓存器缓存最终的 php 代码应该没有任何问题。
  • 模板语言不能仅限于输出变量。总会有一个逻辑块。如果您自己从未使用过模板,为什么要回答这个问题?
  • @Col:嗯,这个模板语言确实支持逻辑块。正如您在我的语法示例中看到的那样,您可以例如使用foreach 循环。 if 或 PHP 支持的任何其他语言结构也是如此。为什么你说我自己从来没有使用过模板? Imho Twig 被认为是模板引擎,因此我认为自己是模板的用户。 PS:你有否否决这个答案Col?
  • 一开始没有注意到。因此,您只需将 &lt;? 更改为 {% 并返回。好吧,它比我一开始想的还要丑。如果可以的话,我会两次否决这个科学怪人。
  • @Col:这正是你会做的。唯一的区别是,{% 将与 short_tags 无关(好吧,我已经知道你对此的看法)。此外,没有人阻止您改用&lt;?&lt;?php。这种“模板语言”允许{% 和本机版本。但你是对的,两种语法都认为是不好的。我可能应该删除{% } 语法,而是将&lt;? ?&gt; 扩展为&lt;?php ?&gt;。我会考虑的,谢谢反馈。
【解决方案3】:

我会做一些愚蠢的事情,并建议一些根本不需要模板引擎的东西,并且每个变量/调用最多只需要比你那里的字符多 5 个字符 - 将 {$foo} 替换为 &lt;?=$foo?&gt; 然后你可以使用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 基本上是没有办法的。

【讨论】:

  • 我考虑过将{{ 替换为&lt;?php 并将}} 替换为?&gt;,但它可能会变得冒险。也许不同的分隔符会更好......
  • 如果你想避免 eval,你可以预处理模板以替换 {...}`` with ` 但这让它有点痛苦,而且有点不太清楚需要各种“编译”
  • 你想要preg_quote() 而不是preg_escape() 吗?
  • @tobyodavies:我不确定,但我认为我们的用户对eval_which_handles_open_and_close_tags 的访问权一样多,因为我隐约记得eval('?&gt;' . $phpfile); 有效。
  • @Col。我的观点是,如果您想避免与 eval 相关的性能问题,您可以使用 php 短标签(或预处理 php 标签,请参阅前面的评论),将自己限制为仅变量替换或使用 eval,或者更糟糕的是,您自己的(或其他人的)迷你语言的解释器请在发表评论之前阅读答案。
【解决方案4】:

没有终极解决方案。每个都有优点和缺点。但是你已经得出了你想要的。这似乎是一个非常明智的方向。所以我建议你找到最有效的方法来实现它。

您基本上只需要将您的文档包含在一些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 模板之间切换,但也有一些具有正常&lt;?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:如果三个&lt;&lt;&lt;heredoc&amp;EOF; 语法行仍然出现太脏,那么最好的无评估选项是使用基于正则表达式的解析器。我不同意这比原生 PHP 慢的普遍神话。事实上,我相信 PHP 标记器和解析器落后于 PCRE。特别是如果它关于插值变量。只是后者不是 APC/Zend 缓存的,你自己在那里。

【讨论】:

    【解决方案5】:

    就我个人而言,我不会使用任何模板系统,因为忘记转义变量会导致远程代码执行漏洞。

    【讨论】:

      【解决方案6】:

      我个人正在使用这个模板引擎: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');   
      ?>
      

      外部模板如下所示:

      <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>
      

      如果你不喜欢/不能使用短标签,那么用 echos 替换它们。这几乎是您所能获得的最简单的东西,同时仍然具有您需要恕我直言的所有功能。

      【讨论】:

      • 好吧,例如,允许电子邮件的字段的未转义输出对于脚本插入 XSS 可能已经成熟,因为电子邮件中可以允许的内容太多了,而且有多种方法可以插入脚本。所以从那里就有一个问题,这就是为什么像这样的模板与模板引擎相比大多是无用的。所以 h($someVar);?> 可能是你能得到的最简洁的,并且在纯 php 中仍然有效(h() 是一个转义函数包装)。我完全赞成模板方法中的纯 php,但人们不断夸大最佳的最小冗长。
      • 嗯,这当然是简化的。安全性和输入处理是一个不同的主题。这里的$email 将来自模型,因此它已经通过了健全性检查。
      【解决方案7】:

      使用函数的非常简单的模板:

      <?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 分离。另外(这是我今天的编码所需要的)我可以将填写好的模板保存起来以输出到文件中(稍后在我的程序中)。

      【讨论】:

        【解决方案8】:

        这是 mustache 的最小实现,仅用于替换变量。

        // 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

        【讨论】:

          猜你喜欢
          • 2013-03-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-03-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多