DEV Community

loading...
Cover image for What a template engine should and shouldn't be

What a template engine should and shouldn't be

sroehrl profile image neoan ・4 min read

package

A little history

A quick glance at smarty and it is abundantly clear that it has been around since PHP 4.x. But why would you use a template engine in the first place?

Well, the initial idea was to have people with little technical knowledge produce templates and designs. And pretty soon functionality was required to facilitate common tasks. Much like we now offer custom elements or React components to web designers. While the need for such tooling changed with less backend rendered content, another advantage still lives: the need for reusable markup applied to various use cases while being cleanly maintained separately from the functionality.

So it is no wonder that to this day template engines like pug, blade or twig still dominate backend-delivered content.

Unfortunately, they have kept some bad habits. It is simply "unclean" to have any kind of functionality in your template that goes beyond reactive/conditional rendering. Most of the functionality most in the field provide should never be used within a template.

Let's look at an example from twig's documentation

 {% set users = [ 
    {first: "Bob", last: "Smith"},
    {first: "Alice", last: "Dupond"},
 ] %}

<h1>Members</h1>
<ul>
    {% for user in users %}
        <li>{{ user.last }},{{ user.first }}</li>
    {% endfor %}
</ul>


Enter fullscreen mode Exit fullscreen mode

This echoes

<ul>
    <li>Smith, Bob</li>
    <li>Dupond, Alice</li>
</ul>

Enter fullscreen mode Exit fullscreen mode

The first thing we notice is the variable declaration "users". So we now have to consider that variables are defined in a template, which is neither good for maintenance nor for re-usability. But granted, this is a constructed example and you would normally pass in these names? Okay. But when is it okay to define variables in a template, then? Think. Think harder. No? But why can you do it then if you shouldn't? Of course there are edge-cases you can come up with, but in 99.9% of the cases you make your debugging and maintenance life harder.

We are now outputting "users" in a loop. We should note that twig offers many filters, so another way of writing the intended outcome is

...
<li>{{ user|reverse|join(',') }}</li>
...

Enter fullscreen mode Exit fullscreen mode

which may save us a few keystrokes but certainly does now help in regards to readability.

Lastly, we seem to have not left the PHP flow of blocks. We should remind ourselves that this example in pure PHP would could look like this:

<h1>Members</h1>
<ul>
   <!-- instead of: {% for user in users %}: -->   
   <?php foreach($users as $user){
      // instead of: <li>{{ user.last }},{{ user.first }}</li>:
      echo "<li>$user['last'],$user['first']</li>";
    // instead of {% endfor %}:
    }?>

</ul>
Enter fullscreen mode Exit fullscreen mode

Not too much of a difference, is it? However, "free" PHP in the template should be avoided and therefore template engine have their legitimacy - unless ...

Unless they are like most of them: expensive overhead packed with functionality we wanted out of the template in the first place.

How could a template engine look like?

Well, we now get into the realm of taste, but as a full-stack developer, the first thing I want to do is to think in elements rather than blocks.

So instead of

{% for user in users %}
   <li>{{user.first}}</li>
{% endfor %}

Enter fullscreen mode Exit fullscreen mode

I want to work like

<li n-for="users as user">{{user.first}}</li>
Enter fullscreen mode Exit fullscreen mode

It is less disturbing and I am used to such a markup and thinking from front-end technology. And, it does save space:

twig

    {% if users %}
    <ul>
        {% for user in users %}
            <li>{{ user.first }}</li>
        {% endfor %}
    </ul>
    {% endif %}
Enter fullscreen mode Exit fullscreen mode

neoan3-apps/template

<ul n-if="users">
   <li n-for="users as user">{{ user.first }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

And what if I need functionality in my template?

As said before, there are certain edge-cases where my initial statement that a template engine should only provide loops and conditions are rightfully challenged. In such cases, rather than relying on a particular set of functions and hope they fulfill your needs, I want to be able to create functionality as needed, while re-usability should be possible:

TemplateFunctions::registerClosure(
   'formatName', 
   fn($user) => ucfirst($user['first']) . ', ' . ucfirst($user['last'])
);
Enter fullscreen mode Exit fullscreen mode

And then use it in my templates:

<ul n-if="users">
   <li n-for="users as user">{{ formatName(user) }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Output:

<ul>
   <li>Bob, Smith</li>
   <li>Alice, Dupond</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Performance

Lastly, we should probably speak about performance and whether or not caching is a necessity. Instead, I will refer to a video of mine to clarify that:

youtu.be/5CqklbC2ZFM

IMAGE ALT TEXT HERE

You will notice that the templating of neoan3 was part of the string operations class back then, but by now neoan3-apps/template is a solid, standalone solution redy to play nice with whatever framework you plug it in.

Curious?

composer require neoan3-apps/template

packagist

github

Lastly,...

I would like to know what you think. Between the relatively straight forward Blade from Laravel to syntactic sugar like Pug: what do you like and why?

Discussion

pic
Editor guide