[Templates] TT3 Template objects (Re: Template::Alloy)

Andy Wardley abw@wardley.org
Fri, 15 Jun 2007 17:06:07 +0100


Paul Seamons wrote:
> I have released a new version of [...] Template::Alloy
 > [...] allows you to use [lots of stuff]

Hi Paul,

First let me say what great work you've done on Template::Alloy.  The 
significance of being able to plug in different template languages is 
certainly not lost on me.  It's always been the #1 priority for TT3 - to 
clearly separate any particular template language from the general purpose 
template processing framework.  This is not only an important milestone 
towards that, but also sets the standard for what I've got to catch up to!

Here's something of a brain dump on how it (works/will work) in TT3.

In TT3, all templates are instances of Template::Template (hereafer T::T).  Or 
rather, they're instances of subclasses of T::T which specialise it for 
different languages:

   Template::Template::TT2
   Template::Template::TT3
   Template::Template::HT  (HTML Template)
   ...etc...

I hadn't considered Velocity or Text::Tmpl until now.  But seeing as you've 
done all the hard work, I guess we can add them to the official list.  I've 
also got PETAL pencilled in as something that would be nice to support.

The T:T base class provides some common methods returning information about 
the template:

   $t->uri()
   $t->name()
   $t->text()
   $t->meta()
   ...etc...

[aside]
The uri() method is worth mentioning because it ties in with another proposal 
you made regarding CACHE_STR_REF.  If a template is loaded from a file then 
uri() will return the filename, or possible a more canonical URI like 
"file:/blah/blah" or even "file://localhost/blah/blah".  If it's generated 
from a text string, then I *was* planning to use the Perl memory address of 
the text ref, although that could fail under certain conditions (e.g. shared 
memory cache between processes) and certainly isn't safe for serialisation. 
So the md5 sum sounds like a much better idea, either enabled by default or 
via a config option.
[/aside]

In TT3, you'll be able to use templates as stand-alone objects.  For example:

     my $f = Template->template( file => 'example.tt3' );
     my $t = Template->template( text => 'Hello [% thing %]' );

     print $t->process( thing => 'World' );
     print $t->process( thing => 'Badger' );

This is one important difference between TT2 and TT3.  TT2 templates require a 
separate context object in order to do anything useful.  TT3 templates will 
effectively *be* their own context, by virtue of the fact that 
Template::Template will be a subclass of Template::Context (or something like 
it - I'm still hacking on that bit ATM).

That means you can do things like this, in HTML-Template style:

    $t->set( thing => 'Badger' );
    $t->get('thing');

Each template will also have it's own methods for fetching and using things 
other than variables:

    $t->template('header');
    $t->plugin('table');
    $t->filter('html');

And so on.  (there's magic behind that which I'll skip over for now)

What happens in the background when you call process(), is that the template 
takes responsibility for parsing and compiling itself into a runtime form, and 
then runs itself.  How it does all that it up to the template (and friends, 
like compilers, caches, stores, etc).

At the low-level end of things, this gives you a lot more flexibility.  It 
means you can bypass the whole Template front-end mechanism (e.g. the service 
layer) if you want, and just get a template object that spits some text back 
out when you feed it variables.

    print $t->process( thing => 'Badger' );

It makes things like this much easier to implement:

    [% "Hello [% thing %]".eval %]
    [% "Hello [% thing %]".template %]
    [% "Hello [% thing %]".process(thing = 'Badger') %]

The other key benefit is that a template object can implement whatever kind of 
parsing *and* runtime strategy that it likes.

Standard TT3 template objects will compile themselves into something like this:

    sub {
        my ($self, $args) = @_;
        return "Hello " . $self->get('thing');
    }

It's almost exactly the same as in TT2, except we're using $self rather than 
$context, and there's a built-in method for get() rather than going through 
the stash.

The difference is that the template ($self) can implement it's own get() 
method, or do something else entirely to handle variables.  It's no longer 
limited by what the $context provides.  Nor is it even required to compile 
itself to Perl code.  It can implement itself using an optree like TT1 or CET.

Having said that, a key part of TT is getting a bunch of templates to play 
nicely with each, rather than skulking around by themselves.  When we INCLUDE 
a template, for example, we want it to get all the variables that we've 
currently got without us having to explicitly pass them over.

TT2 operates on the idea of a shared context.  All variables are available 
everywhere unless you take measures to ensure that they localised.  For 
example, by using INCLUDE rather than PROCESS.  Internally this involves 
cloning the stash (which eats memory and time) and messing with the BLOCKS 
definition list (which is just plain ugly).  And all it really achieves is to 
protect you from a SET in the called template or a BLOCK definition which 
would otherwise screw up your own variable/templates.

TT3 will operate on the idea of separate contexts that know how to play along 
nicely with each other (should they chose to do so).  The outer template in 
the example above will do something like this in its generated code:

    $self->template_include('make_pretty');  # [% INCLUDE make_pretty %]

Which will do this:

    $template = $self->template('make_pretty');

And then this:

    $template->process({ }, context => $self);

The outer template passes a reference to itself as the 'context' for the inner 
template.  The inner get() method can then call the $context->get() method to 
fetch any variables from the outer template and cache them locally.  Although 
that first get() on a variable takes a shade longer, the local cache means you 
only take the hit once.  And you win by only doing this for the variables you 
actually use, whereas the current stash cloning code copies all your 
variables, every time, regardless.

When you set() a variable, you set it in the local variable cache and don't 
affect the calling context.  So you effectively get the protection that 
INCLUDE gives you for free.  It's more like Perl in that sense: don't export 
anything by default!  However, to implement the way PROCESS currently works, 
there will be some mechanism (got a couple of candidates but no firm code yet) 
to allow the template to set stuff in the context.

The other nice thing is that the same strategy works for all the resources: 
variables, templates, plugins, filters, and so on.  There's no need for any 
more messing about with BLOCK definition lists and other special cases like that.

So to cut a long story short, getting all these different template languages 
to play nicely with each other is very much on the TT3 agenda.  Although the 
underlying mechanism in TT3 will be slightly different from what you've got in 
Alloy (as I understand it), it's the same kind of abstraction operating at a 
slightly different level.  And I think that this change in the way the context 
works will make the whole process easier.

Anyway it's certainly very inspiring for me to see a working implementation of 
these ideas in CET/Alloy.  It's serving as both the carrot and the stick 
that's keeping me moving forwards on TT3.

I've got a shiny new version of the TT2 web site and an overhaul of all the 
documentation that I'm about to push out.  The very next task on my TODO list 
is to set up a public subversion repository for the TT3 code so you can get 
your hands on what I've got.  However, I'm interspersing that with Real 
Work[tm] for the next few months so I'll be dropping in and out a bit.

Cheers
A