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

Community
  • 1
  • 1
Dagg Nabbit
  • 75,346
  • 19
  • 113
  • 141
  • 3
    I just... can't... understand... why.... – Stephen Oct 25 '10 at 21:59
  • 5
    No offense, but WTF???? http://thedailywtf.com/Articles/We-Use-BobX.aspx Please don't ever make anyone use this. Learn about MVC. Use css to style. Clean html and VANILLA php are your friends. – Byron Whitlock Oct 25 '10 at 22:04
  • That's not helpful. There are plenty of people who *do* understand why templating is useful. I've given plenty of arguments why. If you don't understand why it's useful, you're probably not going to be able to give a useful answer or make a useful comment. Try reading over some of the arguments here and in the previous thread. – Dagg Nabbit Oct 25 '10 at 22:06
  • 2
    @no There is a reason you have no answers. Meditate on that a bit. – Byron Whitlock Oct 25 '10 at 22:08
  • @Byron Whitlock: I don't see the connection. In fact that bobx stuff looks more like regular php-as-a-template-language to me than this thing. One of the major points was to get away from angle brackets, they screw up syntax highlighting. Another was to keep php syntax intact as much as possible. The 'example' you linked does neither. – Dagg Nabbit Oct 25 '10 at 22:09
  • @Byron Whitlock: I asked this less than 15 minutes ago. I'd expect a decent response to require that much time reading and thinking about the question, maybe playing with the code. Anything else very likely hasn't been thought through. Meditate on that. – Dagg Nabbit Oct 25 '10 at 22:11
  • 1
    PHP's angle brackets don't break my editor (Textmate). Maybe you need a better text editor? Beyond that, this looks horribly convoluted and worse than useless. – ceejayoz Oct 25 '10 at 22:15
  • @Byron Whitlock: You shouldn't throw the MVC buzzword in. Most PHP frameworks are actually based on MVP or PMVC or MD. There's just a huge disconnect from definitions, and nobody has told them yet. – mario Oct 25 '10 at 22:16
  • 2
    @no, as much fun as it is to do stuff like this, when it comes time to maintain the code, your successor will curse you to the high heavens. No matter how elegant you feel it is. That was the point of the bobx link. Sorry if I offended you, but over the years I've had to deal with too many different custom templating engines in php (And tcl and perl etc). Maybe I am getting old, but I have no patience for programmers who do this stuff any more. I truly wish you good luck on your projects, and *please please* leave some documentation around for your templating engine. – Byron Whitlock Oct 25 '10 at 22:22
  • @Byron Whitlock: So your POV is, you don't want to see any custom template engines, because you're sick of them. That's fine. I am looking for answers that accept that templating is a reality. The fact that you'd prefer not to learn some simple syntax is not going to dissuade me. You offer no reasons against templating other than you don't want to learn another templating syntax, and it's more code to maintain. The learning curve here is very shallow, and the code is 150 lines tops. If you dislike templating engines, help create a more tolerable one, or go away. – Dagg Nabbit Oct 25 '10 at 22:53
  • 2
    just the fact that so many developers disagree with this should tell you something. – Galen Oct 25 '10 at 22:58
  • 1
    @Galen: It tells me PHP developers don't want to use templates. Is it because the templating engines that are out there suck, or because templating is somehow wrong? I have trouble believing the latter, so I'm trying to create a templating engine that doesn't suck. – Dagg Nabbit Oct 25 '10 at 23:32
  • 3
    Its not the fact that they all suck its the fact that they arent needed. It's like creating a class that uses a for loop to imitate a foreach. We already have a foreach, why are you making that? We already have a template engine, why are you creating that? – Galen Oct 26 '10 at 00:44

1 Answers1

5

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
  • 176,543
  • 40
  • 303
  • 368
  • 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. – Dagg Nabbit Oct 25 '10 at 22:25
  • 2
    You didn't answer any of his questions, you counter argued about his goals. – mario Oct 25 '10 at 22:25
  • 3
    @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 Oct 25 '10 at 22:30
  • @no Even when short tags are off, isn't much wordier, and it's immediately understood by any PHP dev you take on. – ceejayoz Oct 25 '10 at 22:31
  • 1
    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 Oct 25 '10 at 22:36
  • 3
    @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 Oct 25 '10 at 22:39
  • 1
    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 Oct 25 '10 at 22:40
  • 1
    @ceejayoz: What benefits would you say the mature templating engines have over a solution like this? This was essentially one of my original questions. – Dagg Nabbit Oct 25 '10 at 22:59