16

更新 -すべての応答に感謝します。このQはややこしくなってきているので、誰かが興味を持っているなら続編を始めました.


私は友人のために簡単なスクリプトをまとめていましたが、PHP でテンプレートを作成するための非常に簡単な方法を見つけました。

基本的には、HTML ドキュメントをヒアドキュメント文字列として解析することを目的としているため、その中の変数は PHP によって展開されます。

パススルー関数を使用すると、式の評価と、文字列内の関数および静的メソッドの呼び出しが可能になります。

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

ヒアドキュメント文字列内のドキュメントを解析するコードは、とてつもなく単純です。

$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 ?>タグからジャンプしたりすると、怠惰になり、データベース クエリを実行したり、他のサーバー アクションを実行したりしやすくなります。上記のような方法を使用すると、「ブロック」(セミコロンなし) ごとに 1 つのステートメントしか使用できないため、そのトラップに引っかかるのははるかに困難です。<?= ... ?>ほとんど同じ利点がありますが...

  • 短いタグが常に有効になっているわけではありません

    ...そして、アプリをさまざまな構成で実行したいと考えています。

私が最初にコンセプトを一緒にハックするとき、それは 1 つの php ファイルとして始まります。<?phpしかし、それが大きくなる前に、すべてのphpファイルの最初と?>最後に1つだけがあり、できればすべてがコントローラー、設定、画像サーバーなどを除くクラスである場合を除き、満足できません.

ビューにあまり PHP を入れたくないのは、デザイナーが混乱するからです。

<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 テンプレート" にできるだけ近づける必要があります。私がそれで遊んでいる間、今のところ評価はまだ残っています。ヒアドキュメントは、その役割を少し変更しました。後で詳しく書いて、いくつかの回答に返信しようとしますが、今のところ...

<?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';
}

したがって、基本的には、まずテンプレートをコンパイルしてから実行します。コンパイルは、キャッシュされたテンプレートがまだ存在しない場合、またはキャッシュ内のテンプレートよりも新しいバージョンのテンプレートがある場合にのみ実行されます。

それでは、コンパイルについて話しましょう。出力用と制御構造用の 2 つの構文を定義します。デフォルトでは、出力は常にエスケープされます。エスケープしたくない場合は、「安全」とマークする必要があります。これにより、追加のセキュリティが提供されます。したがって、ここに構文の例を示します。

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

つまり、{ something }何かをエスケープしてエコーするために使用します。{!! something}何かをエスケープせずに直接エコーするために使用します。また、PHPコードをエコーせずに実行するために使用{% command }します(たとえば、制御構造の場合)。

そのためのコンパイルコードは次のとおりです。

$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

究極の解決策はありません。それぞれに長所と短所があります。しかし、あなたはすでにあなたが望むものを結論付けています。そして、それは非常に賢明な方向に思えます。したがって、それを達成するための最も効率的な方法を見つけることをお勧めします。

基本的に、ドキュメントをヒアドキュメントの構文糖衣で囲むだけで済みます。各ファイルの開始時:

<?=<<<EOF

そして、各テンプレート ファイルの最後に:

EOF;
?>

功績賞。しかし明らかに、これはほとんどの構文強調エンジンを混乱させます。オープン ソースのテキスト エディターを修正できます。しかし、Dreamweaver は別物です。したがって、唯一の有用なオプションは、生の $varnames-HTML を含むテンプレートとヒアドキュメントで囲まれたテンプレートとの間で変換できる小さなプリコンパイラ スクリプトを使用することです。これは非常に基本的な正規表現とファイル書き換えのアプローチです。

#!/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 ロジックを備えたテンプレートが必要になります。したがって、一貫した処理を行うには、特別な eval/regex 処理ラッパーを使用せずに、すべてのテンプレートを適切な PHP として動作させる必要があります。これにより、ヒアドキュメント テンプレートを簡単に切り替えることができますが、通常の<?php print出力を持つテンプレートもいくつかあります。必要に応じて組み合わせれば、デザイナーは大部分のファイルで作業できますが、いくつかの複雑なケースは回避できます。たとえば、私がよく使用するテンプレートは次のとおりです。

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

追加のハンドラーはなく、一般的なテンプレート タイプ (生の php ファイルと smartyish html ファイル) の両方で機能します。唯一の欠点は、上記のコンバーター スクリプトを時折使用することです。

extract(array_map("htmlspecialchars",get_defined_vars()));また、セキュリティのために各テンプレートの上に を追加します。

とにかく、あなたのpassthrough方法は非常に賢いと言わざるを得ません。$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>

少し拡張していますが、結局のところ、ヒアドキュメント テンプレートに単純なロジックを含めることは可能のようです。テンプレート ファイレリティにつながる可能性がありますが、最も単純なテンプレート ロジックを適用するのに役立ちます。

トピック外: 3 つの<<<heredoc&EOF;構文行がまだ汚いように見える場合、最適な評価なしオプションは、正規表現ベースのパーサーを使用することです。ネイティブ PHP より遅いという一般的な神話には同意しません。実際、PHP トークナイザーとパーサーは PCRE に遅れをとっていると思います。特に、変数の補間のみに関するものである場合。後者は APC/Zend にキャッシュされていないというだけです。

于 2010-10-17T11:23:56.203 に答える
2

個人的には、変数をエスケープするのを忘れると、リモートでコードが実行される脆弱性が生じるようなテンプレート システムには一切触れません。

于 2010-10-18T16:01:12.517 に答える
0

個人的には、このテンプレート エンジンを使用しています: http://articles.sitepoint.com/article/beyond-template-engine/5

特にシンプルなため、とても気に入っています。それはあなたの最新の化身にちょっと似ていますが、私見は、ヒアドキュメントを使用して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>

短いタグが気に入らない、または使用できない場合は、エコーに置き換えてください。これは、私見に必要なすべての機能を備えながら、できる限りシンプルに近いものです.

于 2010-10-23T15:58:58.587 に答える