Dirt-простые шаблоны PHP... может ли это работать без `eval`?

Обновление - Спасибо за все ответы. Этот Q становится грязным, поэтому я начал продолжение, если кому-то интересно.


Я собрал быстрый script для друга и наткнулся на действительно простой способ создания шаблонов в 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/javascript, кэшировать его, запускать через шаблон xsl, что угодно.

  • Хороший дизайн MVC

    Templating способствует разделению зрения от модели и контроллера.

    При прыжке с тегами <?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 templating", при этом шаблоны являются синтаксическим сахаром. 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>

Ответы

Ответ 1

PHP сам был изначально предназначен как язык шаблонов (т.е. простой способ разрешить вам встраивать код внутри HTML).

Как вы видите из ваших собственных примеров, это слишком сложно, чтобы оправдать использование таким образом большую часть времени, поэтому хорошая практика отошла от этого, чтобы использовать его больше как традиционный язык и только вырваться из <?php ?> как можно меньше.

Проблема заключалась в том, что люди все еще хотели использовать язык шаблонов, поэтому были созданы платформы, такие как Smarty. Но если вы посмотрите на них сейчас, Smarty поддерживает такие вещи, как свои собственные переменные и петли foreach... и вскоре шаблоны Smarty начинают иметь те же проблемы, что и используемые шаблоны PHP; вы, возможно, просто использовали родной PHP в первую очередь.

То, что я пытаюсь сказать здесь, состоит в том, что идеалы простого языка шаблонов на самом деле не так понятны. Практически невозможно сделать его достаточно простым, чтобы не отпугивать дизайнеров и в то же время дать ему достаточно гибкости, чтобы на самом деле делать то, что вам нужно.

Ответ 2

Я собираюсь сделать что-то глупое и предложить что-то, что вообще не требует шаблонизатора, и требует только не более 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, не допрограммируя.

Ответ 3

Если вы не хотите использовать большие движки шаблонов, такие как 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';
}

Я не тестировал вышеуказанный код;) Это только основная идея.

Ответ 4

Нет окончательного решения. У каждого есть плюсы и минусы. Но вы уже пришли к выводу, чего хотите. И это кажется очень разумным направлением. Поэтому я предлагаю вам найти наиболее эффективный способ его достижения.

В основном вам нужно заключить документы только в синтаксическом сахаре heredoc. В начале каждого файла:

<?=<<<EOF

И в конце каждого файла шаблона:

EOF;
?>

Награда за достижения. Но, очевидно, это смущает большинство синтаксических движков подсветки. Я могу исправить свой текстовый редактор, это с открытым исходным кодом. Но Dreamweaver - это совсем другое дело. Поэтому единственным полезным вариантом является использование небольшого pre-compiler script, который может конвертировать между шаблонами с необработанными $varnames-HTML и шаблонами, заключенными в Heredoc. Это очень простой подход к regex и перезаписи файлов:

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

Отсутствует дополнительный обработчик и работает как для общих типов шаблонов (raw php и smartyish html файлов). Единственным недостатком является случайное использование упомянутого преобразователя script.

Я также добавил 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. Может привести к шаблону-fileritis, но помогает обеспечить наиболее простую логику шаблона.

Offtopic: Если три строки синтаксиса <<<heredoc&EOF; все еще выглядят слишком грязными, лучшая опция no-eval использует парсер, основанный на регулярном выражении. Я не согласен с общим мифом, что медленнее, чем собственный PHP. На самом деле я считаю, что токенизатор PHP и парсер отстают от PCRE. Особенно, если он исключительно об интерполирующих переменных. Это просто, что последний не является APC/Zend-кэшем, вы бы сами по себе.

Ответ 5

Лично я не трогал бы палкой любую систему шаблонов, где забывание избежать переменной создает уязвимость удаленного выполнения кода.

Ответ 6

Лично я использую этот механизм шаблонов: http://articles.sitepoint.com/article/beyond-template-engine/5

Мне это очень нравится, особенно из-за простоты. Это похоже на ваше последнее воплощение, но IMHO - лучший подход, чем использование 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>

а внутренний (входит в переменную outter template $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. Это как можно ближе к грязи, как вы можете получить, но при этом все функции вам понадобятся IMHO.

Ответ 7

Dead-simple templating с использованием функции:

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

Это минимальная реализация усов, чтобы просто заменить переменные.

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