{{macro}} - a zillionth attempt to make a PHP template engine that sucks less

After having used multiple template engines for the last 5 years I’m a strong believer that the best template engine is…PHP itself :) We just need some tools to make the usage of raw PHP in templates more friendly and readable.

I have used the forked version of WACT template engine for about 3 years and finally come to the conclusion that its runtime part is way too complicated and non obvious at times. However I do very like the idea of compile time tree of components which generate PHP code. At the same time I just wanted to simplify WACT and make the process of adding new tags much more straight-forward.

That’s why I have hacked up the initial implementation of {{macro}}. To be short, the main idea behind {{macro}} is to use C/C++ alike(but way more sophisticated) macro tags to simplify usage of raw PHP in templates.

So, technically, {{macro}} is a simplified WACT without the run-time components part. Although {{macro}} has a components tree at compile time only, its runtime part is PHP.

{{helloworld}}

All {{macro}} tags are not hardcoded as some special cases and new tags can be added quite easily. For example {{helloworld}} tag can be written as follows:

helloworld.tag.php

<?php
/**
 * @tag helloworld
 */
class HelloWorldMacro extends lmbMacroTag
{
  function generateContents($code)
  {
    $code->writeHTML('<b>');
    $code->writePHP('echo "Hello world!";');
    $code->writeHTML('</b>');
  }
}

Now this tag, once put into special directory scanned by {{macro}}, can be used in template:

{{helloworld}}

…which, when rendered, should output: <b>Hello World!</b>

Tags and output expressions

As you might have guessed, tags are defined with {{some_name}} syntax(hence the name {{macro}}). The choice for this non-xml-alike syntax was intentional: {{macro}} doesn’t care about XML alike markup at all and allows to have the following constructs in templates:

<a src="{{link_to ..}}"></a>

Apart from tags there are output expressions which have {$foo} alike syntax. Output expression is just a convenient way to write <?php echo .. ?> in template. For example:

{$foo}

…is compiled into:

<?php echo $foo;?>

However output expressions can do a lot more. For example, output expressions support properties chaining:

{$author.book.title}

…which will be approximately rendered into the following PHP code:

<?php echo $author->get('book')->get('title');?>

Output expressions also support filters:

{$author.name|trim|uppercase}

…here’s is the PHP code which will be rendered:

<?php echo uppercase(trim($author->get('name')));?>

Output filters

Filters are also not special cases and can be added quite easily too, have a look at the possible implementation of uppercase filter:

class UpperFilter extends lmbMacroFilter
{
  function getValue()
  {
    return 'strtoupper(' . $this->base->getValue() . ')';
  }
}

And, by the way, output expressions can be used in macro tags attributes too:

{{list from="{$author.books}"}}
...
{{/list}}

BTW, big thanks goes to Sergey “syfisher” Yudin who ported WACT alike filters and output expressions to {{macro}}!

{{list}} tag

Let’s have a look at some real world examples where {{macro}} makes usage of PHP in templates way more pleasant. Here’s some PHP code in a template which renders some list of items:

<?php
$count=0;
foreach($items as $item) {
$count++;
if($count == 1) {
?>
Items
<?php } ?>
<?php echo $count;?> )<b><?php echo $item;?></b>
<?php } ?>
<?php if(!$count){ ?>
Nothing
<?php } else { ?>
done!
<?php } ?>

Don’t you think it’s quite unreadable? Let’s replace it with {{macro}} equivalent using {{list}} macro tag:

{{list for="$items" as="$item" counter="$count"}}
Items:
{{list:item}}
{$count})<b>{$item}</b>
{{/list:item}}
done!
{{list:empty}}
Nothing
{{/list:empty}}
{{/list}}

{{include}} and {{wrap}} tags

If you have ever tried WACT, you might be interested to know that {{list}} tag is a port of WACT’s <list:list>. Apart from basic {{list}} macro tag there are also {{include}} and {{wrap}} tags available.

{{include}} macro works almost like WACT’s <core:include>:

{{include file="foo.phtml"/}}

In this case contents of foo.phtml will be placed into the current template at compile time.

{{include}} supports passing local variables into included templates:

{{include foo="foo" bar="$bar" wow="$this->wow"/}}

All passed attributes will be available in the included template as variables: $foo, $bar and $wow. This allows to have partials with {{macro}} quite easily.

{{include}} also supports dynamic file includes as well:

{{include file="$this->file"/}}

In this case $this->file template will be included in runtime. $this->file value can be passed into template using the following code:

...
$macro = new lmbMacroTemplate("page.phtml");
$macro->set("file", "include.phtml");
$macro->render();

Basically this is the minimal interface which {{macro}} provides. Whatever one needs to pass into template is passed via set($name, $value) interface and available in template as an attribute of the lmbMacroTemplateExecutor object which can be accessed via $this(e.g. $this->foo).

As I said above there’s also a {{wrap}} macro. It can be used as follows:

page.phtml

{{wrap with="layout.phtml" into="content"}}
Some contents...
{{/wrap}}

layout.phtml

{{slot id="content"/}}

Multiwrapping is supported as well:

page.phtml

{{wrap with="layout.phtml"}}
{{into slot="slot1"}}
  Some contents...
{{/into}}
{{into slot="slot2"}}
  Some contents 2...
{{/into}}
{{/wrap}}

layout.phtml

{{slot id="slot1"/}}
blah-blah
{{slot id="slot2"/}}

As in WACT, {{wrap}} is compiled statically in one template in case if static wrapping is used. However, unlike WACT’s <core:wrap> {{wrap}} supports dynamic wrapping as well:

page.phtml

{{wrap with="$this->layout" into="content"}}
Some contents...
{{/wrap}}

{{apply}} tag

I also added {{template}} and {{apply}} tags. They allow to have “inline” definitions of templates which can be dynamically “applied”:

{{template name="tpl1"}}
Hello, {$name}
{{/template}}

{{template name="tpl2"}}
Good bye, {$name}
{{/template}}

{{apply template="tpl1" name="Bob"/}}
{{apply template="tpl2" name="Bob"/}}

Currently I’m working on {{pager}} and {{form}}(with friends: {{input}}, {{textarea}}, etc) macro tags. Once they are ready I will release macro-0.1.0.

Check it out

At this moment {{macro}} is available via SVN only:

https://svn.limb-project.com/3.x/trunk/limb/macro

Here’s a quick guide on how to try {{macro}}:

  1. Create ~/macro-try directory
  2. Create ~/macro-try/lib/limb directory and proceed there
  3. Checkout the necessary Limb3 packages by running the following commands in console:
    $ svn co https://svn.limb-project.com/3.x/trunk/limb/core core
    $ svn co https://svn.limb-project.com/3.x/trunk/limb/fs fs
    $ svn co https://svn.limb-project.com/3.x/trunk/limb/toolkit toolkit
    $ svn co https://svn.limb-project.com/3.x/trunk/limb/macro macro
    
  4. Create ~/macro-try/templates directory
  5. Create ~/macro-try/var directory
  6. Create template file ~/macro-try/templates/test.phtml as follows:
    {{list for="$#items" as="$item"}}
    List of items:
    {{list:item}}
    {$item}
    {{/list:item}}
    {{/list}}
    

    Tip: $#items is just a shortcut for $this->items

  7. Create PHP script ~/macro-try/test.php as follows:
    <?php
    set_include_path(dirname(__FILE__) . PATH_SEPARATOR .
                     dirname(__FILE__) . '/lib/');
    
    require_once('limb/macro/common.inc.php');
    
    $macro = new lmbMacroTemplate("test.phtml", new lmbMacroConfig(dirname(__FILE__) . '/var'));
    $macro->set("items", array("foo", "bar", "hey"));
    echo $macro->render();
    
  8. Finally, try executing this script in console:
    $ php test.php
    

    You should see the following:

    List of items:
    
    foo
    
    bar
    
    hey
    
  9. Jump for joy - you did it ;)

You may also want to have a look at {{macro}} unit tests which may tell way more about this template engine:

https://svn.limb-project.com/3.x/trunk/limb/macro/tests

7 Responses to “{{macro}} - a zillionth attempt to make a PHP template engine that sucks less”

  1. korchasa Says:

    layout.phtml
    В примерах не закрыты теги slot

  2. pachanga Says:

    Спасибо!

    P.S. How about commenting in English? ;)

  3. My desperate life quest for efficiency » Blog Archive » Limb3 2007.4(Frozzy) Released! Says:

    […] in this release, I’m especially proud of finally releasing the first version of {{macro}}(I blogged about it before). Sergei Yudin added {{macro}} tags for form elements and pager right before the release. […]

  4. My desperate life quest for efficiency » Blog Archive » {{macro}} online usage examples Says:

    […] you are wondering who in the world in his sane mind might need yet another PHP templating engine, I blogged about {{macro}} some time ago where, I hope, I presented some unique arguments in its favour. Here’s the […]

  5. conf Says:

    There is a “

  6. conf Says:

    Blog engine eated my first comment :(
    There is a “<php” typo in the first listing after {{list}} tag.

  7. pachanga Says:

    Fixed, thanks!

Leave a Reply