Making custom tags in Twig

I’ve been playing around with Twig in the last few weeks. I was in need of a template parser and wanted to avoid Smarty as I’ve had unpleasant experiences with it in the past, which lead me to Twig. If you don’t know about Twig, its a template parser written by Fabien Potencier of Symfony fame, and its quite good. It sports syntax similar to Jinja and many great features that are not available in Smarty.

Twig offers three main ways of adding your own code, they are filters, macros, and tags. Macros are much like snippets, and filters are very similar to Smarty filters if you’ve ever used it. Both macros and filters have good documentation and are easy to build. Tags however, have much sparser documentation, and creating one requires knowing about tokens, token streams, parsers, and compilers. Don’t loose all hope though, since 0.9.7 there has been a class called Twig_SimpleTokenParser. It makes adding new tags much simpler. After grabbing the HelperTokenParser from the Symfony twig bundle, you’ll need to create an extension.

Show Plain Text
  1.  
  2. <?php
  3. require dirname(dirname(__FILE__)) . '/TokenParsers/HelperTokenParser.php';
  4.  
  5. class CustomHelpers extends Twig_Extension
  6. {
  7.     function getTokenParsers() {
  8.         return array(
  9.             // {% script 'js/jquery.js' %}
  10.             new HelperTokenParser('script', '<script> [with <arguments:array>]', 'html', 'script'),
  11.         );
  12.     }
  13.  
  14.     function getName() {
  15.         'custom.helpers';
  16.     }
  17. }
  18. ?>
  19.  

The above defines a simple extension that only provides a single helper to create script elements. Thus far it was pretty simple. However, the grammar strings left me scratching my head, and we’ll get back to that. Next up I made a simple class that just printed out its arguments so I could see how things worked. Meet the amazing ‘HtmlHelper’ class.

Show Plain Text
  1. class HtmlHelper {
  2.     function script() {
  3.         return 'script() ' . print_r(func_get_args(), true);
  4.     }
  5. }

This class was intended to be completed later. After I made the helper, I made a simple script to wire things together, and a simple template that contained my {% script 'js/jquery.js' %} tag.

Show Plain Text
  1. require '../Twig/lib/Twig/Autoloader.php';
  2. Twig_AutoLoader::register();
  3.  
  4. require './extensions/CustomHelpers.php';
  5. require './Helpers/Html.php';
  6.  
  7. $loader = new Twig_Loader_Filesystem('./templates');
  8. $twig = new Twig_Environment($loader);
  9. $twig->addExtension(new CustomHelpers());
  10.  
  11. $helpers = array(
  12.     'html' => new HtmlHelper()
  13. );
  14.  
  15. $template = $twig->loadTemplate('test_page.tpl');
  16. $template->display(array(
  17.     'name' => 'Mark',
  18.     'list' => array(
  19.         'one', 'two', 'three'
  20.     ),
  21.     '_view' => (object)$helpers
  22. ));

I used the _view template variable as that’s what the class I borrowed from Symfony used, and I wasn’t interested in changing it. When run, the script contained a nice print out of the arguments passed to the {% script %} tag.

Tag grammar

As mentioned earlier, there wasn’t much documentation regarding tag grammars. I did some digging and came up with the following. There are several built in types and constructs you can use in your tag grammar.

  • <foo> Creates an argument that will consume anything. Failing to supply all the required arguments will cause an error.
  • <foo:array> Creates an argument that expects an array, or a hash. Putting an incorrect type will cause errors.
  • [<param1>] Creates an optional argument set. This will accept any number of parameters separated by spaces. This part of the grammar can also contain static strings like <foo> [with <bar>].
  • <foo:expression> Creates an argument that excepts any Twig expression.
  • Tag grammar can also have static strings in it allowing you to make syntax that reads like a DSL. An example would be <name> [with <title>]. This tag would accept both 'foo' and 'foo' with 'bar' as valid calls. You would not able to supply the second argument without the ‘with’ string though.

There are also :number and :body types but I wasn’t quite able to then working. :number would acted oddly, and :body resulted in parse errors. But it could just be that I was doing something wrong. Overall creating simple tags with Twig_SimpleTokenParser was reasonably easy, and using it should help unlock some of the power hiding inside Twig.

Comments

Hi Mark, thanks for pointing out twig. Looks nice!

In what context have you been playing with it? Have you considered using it with Cake? or Lithium?

Will

Will on 4/8/10

Will: I’ve just been looking at it in general. If I have the time, I might try and get a TwigView built for Cake, but no guarantees.

mark story on 5/8/10

Looks great to be used with Cake for probably anything but forms :)

ionas on 10/8/10

Cool post. Thanks!

I’ve gotten a basic TwigView class up and running in cake, but I am getting confused with making child templates work better.

In cake, the view is rendered first and then the layout. Twig can bipass this by using a layout as a parent template, but it restricts you to using one parent (ie. if your view extends your default template you can’t change the layout on the fly in a controller, you’d have to create a separate view for this case). Using parent/child templates is something I am still experimenting with. If I had something more solid I would share :/

Paul Redmond on 11/8/10

@ionas Twig is actually quite elegant in this regard. You can pass objects to templates and call their methods. So using the html helper in twig would look like this:

html.link(‘text’, [‘controller’: ‘users’, ‘action’: ‘index’], [‘class’: ‘my-class’])

Paul Redmond on 11/8/10

Thanks! This was a useful post even though I ended up using the infamous trial-and-error method alongside with dozens of print_r and echo commands.

As a side note: It seems to me that the parser method look() does not work as expected and should be avoided or rewritten. In my tests it seemed to consume a token though it should not do that. If one uses it it will break the parsing completely.

I am converting my Smarty based simple CMS to Twig just to see the possible benefits. So far I am pretty surprised and really like the possibility to put my fingers in the code generation phase. The tag extension is of type

{% cmsmodule name=‘calendar_small’ show_week_numbers=‘true’ week_start=‘monday’ %}

This should create an instance of calendar_small_module and initialize it with the parameters. I have about 20 different modules written in Smarty so far but I think that the conversion will be pretty trivial.

markku on 17/8/10

Have your say:

*
* You can use Textile markup, but be reasonable