16

更新 -感谢所有回复。这个Q有点乱,所以如果有人感兴趣,我开始了续集。


我正在为朋友编写一个快速脚本,偶然发现了一种在 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 设计

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

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

  • 短标签并不总是启用

    ...并且我们希望我们的应用程序能够在各种配置上运行。

当我最初将一个概念组合在一起时,它从一个 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>
4

8 回答 8

28

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

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

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

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

于 2010-10-18T11:26:26.027 回答
11

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

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

于 2010-10-17T18:28:38.127 回答
10

我会做一些愚蠢的事情,并建议一些根本不需要模板引擎的东西,并且每个变量/调用最多只需要比你那里的字符多 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 基本上是没有办法的。

于 2010-10-14T04:27:09.100 回答
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模板中有简单的逻辑。可能导致模板文件炎,但有助于执行最简单的模板逻辑。

题外话:如果三个<<<heredoc&EOF;语法行仍然显得太脏,那么最好的 no-eval 选项是使用基于正则表达式的解析器。我不同意这比原生 PHP 慢的普遍神话。事实上,我相信 PHP 标记器和解析器落后于 PCRE。特别是如果它只是关于插值变量。只是后者不是 APC/Zend 缓存的,你自己在那里。

于 2010-10-17T11:23:56.203 回答
2

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

于 2010-10-18T16:01:12.517 回答
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 分离。此外(这是我今天的编码所需要的)我可以将填写好的模板保存起来以输出到文件中(稍后在我的程序中)。

于 2013-05-22T17:37:29.310 回答
0

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

于 2015-04-21T03:54:11.863 回答
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');   
?>

外部模板如下所示:

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

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

于 2010-10-23T15:58:58.587 回答