views:

53

answers:

1

Background


So, last time I inquired about PHP templates, I got a lot of responses like:

  • it isn't needed; PHP is a good enough templating language on its own.
  • it's hard to develop a templating language that is both powerful and easy for designers to work with (or around).
  • it's already been done, use templating framework X.
  • you're stupid.

All of these points have some amount of validity to them. Keeping them in mind, I went ahead with the templating thing, and now I'm back with more questions. :)

Overview


Goals

Here are the goals for this templating engine:

  • minimal syntax.
  • produce clean php code.
  • don't break html syntax highlighting.
  • no need for php developers to learn anything new (well, not much).
  • support most of php flow control (everything but do..while).
  • support inline php.

Hopefully that sounds pretty good. Notice that not among the goals are things like "prevent template authors from doing X" or "templates will be supplied by anonymous users." Security is not a major concern here, any more than it would be on a normal non-templated php file.

Rules

  • default escape sequence is {{...}}.*
    • if no other rules match, echo or evaluate the sequence
      • if sequence ends with a semicolon, evaluate entire sequence
      • otherwise, echo the first expression and evaluate the rest
  • {{for|foreach|if|switch|while (...):}} begins a block.
    • parentheses in condition may be omitted
    • colon may be omitted
    • outer right bracket may be omitted for bracket matching.**
  • {{else|elseif|break|continue|case|default}} do what you'd expect.
    • parentheses in condition may be omitted
    • outer right bracket may be omitted on {{case}} for bracket matching.
    • outer left bracket may be omitted on {{break|continue}} for bracket matching.
  • {{end}} ends a block.
    • word characters may be appended to 'end', e.g. 'end_if'
    • outer left bracket may be omitted for bracket matching.

* custom brackets can be used.
** bracket matching syntax can be disabled.

Templating

So far we've really just come up with a replacement syntax for <?php...?> and <?=...?>. For this to really be useful, we need some templating-specific operations.

Another templating framework I worked on uses a simple container/content paradigm that should work well here. That templating system was xml-based, so the code would look something like this...

<!-- in a template -->
<html>
  <head>
    <tt:Container name="script" />
  </head>
  <body>
    <tt:Container name="main" />
  </body>
</html>

<!-- in a page -->
<tt:Content name="script">
  <script src="foo.js"></script>
</tt:Content>
<tt:Content name="main">
  <div>...</div>
</tt:Content>

Multiple declarations of a content area with the same name will replace the previous content, but the previous content will be available within the Content tag via Container, so:

<tt:Content name="script">
  <script src="foo.js"></script>
</tt:Content>
...
<tt:Content name="script">
  <script src="bar.js"></script>
  <tt:Container name="script" />
</tt:Content>
...
<tt:Container name="script" />

Should output:

  <script src="bar.js"></script>
  <script src="foo.js"></script>

I've tried to recreate Content and Container via set and get tags in this new templating system. They're intended to work exactly the same way, except, of course, they're not xml tags.

Code


Without further ado:

<?php

class Detemplate {

  public  $container_prefix='_tpl_';
  public  $brackets='{}';
  public  $bracket_matching=true;
  public  $use_cache=false;

  private $block_keywords=array('for','foreach','if','switch','while');
  private $set_count;
  private $get_count;

  public function parse_file ($file, $vars=array()) {
    $sha1=sha1($file);
    $cache = dirname(__FILE__)."/cache/".basename($file).".$sha1.php";
    $f = "{$this->container_prefix}page_{$sha1}_";
    if (!$this->use_cache || !file_exists($cache) || filemtime($cache)<filemtime($file)) {
      $php =  "<?php function $f {$this->t_vars()} ?>".
              $this->parse_markup(file_get_contents($file)).
              "<?php } ?>";
      file_put_contents($cache, $php);
    }
    include $cache;
    $f($vars);
  }

  public function parse_markup ($markup) {

    $blocks=implode('|', $this->block_keywords);

    $arglist= '\s*[\s(](.*?)\)?\s*';  // capture an argument list
    $word=    '\s*(\w+)\s*';          // capture a single word

    $l='\\'.$this->brackets{0}; // left bracket
    $r='\\'.$this->brackets{1}; // right bracket

    $dl="#$l$l";
    $sl=$this->bracket_matching ? "#$l?$l" : $dl;
    $dr="$r$r(?!:$r)#"; 
    $sr=$this->bracket_matching ? "$r$r?(?!:$r)#" : $dr; 

    $markup=preg_replace_callback(
      array (
        $sl.'(end)[_\w]*\s*;?\s*'.$dr,
        $dl.'(el)se\s*if'.$arglist.':?\s*'.$dr,
        $dl.'(else)\s*:?\s*'.$dr,
        $dl.'(case)'.$word.':?\s*'.$sr, 
        $dl.'(default)()\s*:?\s*'.$sr,
        $sl.'(break|continue)\s*;?\s*'.$dr,
        $dl.'(set)'.$word.':?\s*'.$sr, 
        $dl.'(get)'.$word.':?\s*'.$dr, 
        $dl.'(parse)'.$word.':?\s*'.$dr, 
        $dl.'(function|fn)'.$word.$arglist.':?\s*'.$sr,
        $dl.'('.$blocks.')'.$arglist.':?\s*'.$sr,
        '#('.$l.$l.')(.+?)(;?)\s*'.$dr, 
        '#\s*(\?)>[\s\n]*<\?php\s*#',
      ),
      array($this, 'preg_callback'),
      $markup);

    return $markup;

  }

  private function preg_callback ($m) {

    switch ($m[1]) {

      // end of block

      case "end":
        return "<?php } } ?>";

      // keywords with special handling

      case "el": // elseif
        return "<?php } elseif ({$m[2]}) { ?>";
      case "else":
        return "<?php } else { ?>";

      case "case": case "default":
         return "<?php {$m[1]} {$m[2]}: ?>";

      case "break": case "continue":
        return "<?php {$m[1]}; ?>";

      // parse an external template document

      case "parse":
        return $this->parse_markup(file_get_contents($m[2]));

      // save / load content sections

      case "set":
        $i=++$this->set_count[$m[2]];
        $f=$this->t_fn($m[2], $i);
        $p=$this->t_fn($m[2], $i-1);
        $v=$this->t_fn_alias($m[2]);
        return  "<?php if (!function_exists('$f')) { $v='$f'; ".
          "function $f {$this->t_vars()} unset ($v); $v='$p'; ?>";

      case "get":
        $i=++$this->get_count[$m[2]];
        $c=$this->t_fn_ctx($m[2], $i);
        $v=$this->t_tmp();
        $a=$this->t_fn_alias($m[2]);
        return  "<?php if (!$c) { ".
          "foreach (array_keys(get_defined_vars()) as $v) $c".  
          "[$v]=&\$$v; unset($v); } $a(&$c); ?>";


      case "function": case "fn":
        return "<?php if (!function_exists('{$m[2]}')) { ".
          "function {$m[2]} ({$m[3]}) { ?>";

      // echo / interpret

      case "{{":
        return "<?php".($m[3]?"":" echo")." {$m[2]}; ?>";

      // merge adjacent php tags

      case "?": 
        return " ";
    }

    // block keywords
    if (in_array($m[1], $this->block_keywords)) {
      return "<?php { {$m[1]} ({$m[2]}) { ?>";
    }

  } 

  private function t_fn ($name, $index) {
    if ($index<1) return "is_null";
    return "{$this->container_prefix}{$name}_$index";
  }

  private function t_fn_alias ($name) {
    return "\${$this->container_prefix}['fn_$name']";
  }

  private function t_fn_ctx ($name, $index) {
    return "\${$this->container_prefix}['ctx_{$name}_$index']";
  }

  private function t_vars () {
      $v=$this->t_tmp();
      return "($v) { extract($v); unset($v);";
  }

  private function t_tmp () {
      return '$'.$this->container_prefix.'v';
  }


}

?>

Example templated html:

<script>var _lang = {{json_encode($lang)}};</script>
<script src='/cartel/static/inventory.js'></script>
<link href='/cartel/static/inventory.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='#{{urlencode($lang['T_FILTER_ALL'])}}' onclick='applyFilter();'>{{$lang['T_FILTER_ALL']}}</a>
  {{foreach ($filters as $f)}}
    <a href='#{{urlencode($f)}}' onclick='applyFilter("c_{{urlencode($f)}}");'>{{$f}}</a>
  {{end}}
  </div>

  <table class="inventory" id="inventory_table">  

  {{foreach $row_array as $row_num=>$r}

    {{if $row_num==0}

    <tr class='static'>
      {{foreach $r as $col}
      <th>{{$col}}</th>
      {end}} 
      <th class='ordercol'>{{$lang['T_ORDER']}}</th>
    </tr>

    {{else}}


    {{function spin_button $id, $dir, $max}
    <a href='#' class='spinbutton' 
       onclick="return spin('{{$id}}', {{$dir}}, {{$max}})">
      {{$dir==-1 ? '&#x25C0;' : '&#x25B6;'}}
    </a>
    {end}}

    <tr class="{{'c_'.urlencode($r[$man_col])}}">
      {{foreach $r as $i=>$col}
      <td class='{{$i?"col":"firstcol"}}'>{{$col}}</td>
      {end}}
      <td class='ordercol'>
        {{$id="part_{$r[$part_col]}"; $max=$r[$qty_col];}}
        {{spin_button($id, -1, $max)}}
        <input  onchange="spin(this.id, 0, '{{$max}}')" 
                id='{{$id}}' name='{{$id}}'type='text' value='0' />
        {{spin_button($id, +1, $max)}}
      </td>
    </tr>

    {end}}


  {end}}

    <tr class="static"><th colspan="{{$cols+1}}">{{$lang['T_FORM_HELP']}}</th></tr>

  {{foreach $fields as $f}

    <tr class="static">
      <td class="fields" colspan="2">
        <label for="{{$f[0]}}">{{$f[1]}}</label>
      </td>
      <td class="fields" colspan="{{$cols-1}}">
          <input name="{{$f[0]}}" id="{{$f[0]}}" type="text" />
      </td>
    </tr>

  {end}}

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

Questions


I've got a few questions about how to proceed with this thing. Some have definite answers, some may be more CW material...

  • set/get produces messy code. Can it be improved? I'm looking for some kind of sensible middle ground between set/get and {{function}} (see code and example).

  • What's missing that's provided in popular templating languages?

  • Is the syntax ok? Should the lines that echo things, the lines that do things, and the flow control lines be more syntactically different? How about the optional outer brackets for matching... silly?

Looking forward to hearing everyone's input on this.

+2  A: 

minimal syntax.

<?=$variable?> and http://phptemplatinglanguage.com/

produce clean php code.

This doesn't look very clean to me.

don't break html syntax highlighting.

You're breaking PHP syntax highlighting, which I find more problematic than breaking HTML syntax highlighting. If you get a better editor that understands how PHP and HTML interact (I use Textmate) this isn't even a concern.

no need for php developers to learn anything new (well, not much).

Normal PHP already qualifies.

support most of php flow control (everything but do..while).

Normal PHP supports all PHP flow control.

support inline php.

Normal PHP supports inline PHP.

In summary, I see no benefits to this approach, certainly not over the mature existing PHP frameworks and templating engines.

ceejayoz
1-short tags may be disabled. 2-I didn't show the produced php code. It's not as clean as the templated html, admittedly... but it's close to what you'd be writing if we did things this way. In other words, the above code is cleaner. 3-True, but this is supposed to be mostly html. Ideally there would be very few escaped php code sections compared to the surrounding html. My example is just a test.
no
You didn't answer any of his questions, you counter argued about his goals.
mario
@mario "I've got a few questions about how to proceed with this thing" and "Looking forward to hearing everyone's input on this". My answers to those two components are "don't" and "dear God".
ceejayoz
@no Even when short tags are off, <?php echo $variable ?> isn't much wordier, and it's immediately understood by any PHP dev you take on.
ceejayoz
I might agree on the general "don't", but it's clear that these suggestions aren't helpful and were explicitely not asked for.
mario
@mario, i think they are very helpful. Why write a template engine on top of a template engine? Why stop there? Why not make a template engine on top of smarty which is on top of php?
Galen
Sometimes, the best answer to "how do I prevent it from hurting if I smack my head with this hammer" is "don't hammer your head".
ceejayoz
@ceejayoz: What benefits would you say the mature templating engines have over a solution like this? This was essentially one of my original questions.
no