Five New Features in Latte 3.1 That Will Make Your Life Easier

2 hours ago by David Grudl  

Latte 3.1 brings five new features – new filters |column, |commas and |limit, improved |slice for iterators, and two new engine features. No revolution, just small things you'll appreciate.

No More Variable Leaking from {foreach}

This has bugged me for years. You write {foreach $items as $item}, the loop ends, and $item happily lives on with the value of the last element. Worse, it can overwrite a variable you were counting on:

{var $name = 'Yay'}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* oops, no longer 'Yay' *}

Latte has long warned you when foreach overwrote a variable passed to the template via parameters. But variables created directly in the template, e.g. via {var}, were overwritten silently. And that's the sneaky case.

Now you can fix it with a single setting:

$latte->setFeature(Latte\Feature::ScopedLoopVariables);

Variables from {foreach} now exist only inside the loop. After the loop ends, the original value is restored. And if the variable didn't exist before? It disappears completely.

{var $name = 'Yay'}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* outputs 'Yay', yay! *}

And the old warning about overwritten parameters? It gets disabled with this feature, because it no longer makes sense – nothing gets overwritten anymore.

It works with destructuring {foreach $data as [$a, $b]} and of course with keys {foreach $arr as $k => $v} – everything is cleaned up after the loop. Nested loops have independent scopes.

The only exception: if you iterate by reference {foreach $arr as &$v}, scope doesn't apply – references directly modify the original array, so restoring values after the loop would break them. Makes sense.

Indent Your Templates However You Like

You know the drill: you have a <ul> with a {foreach} inside generating <li>. You naturally indent it for readability. But that indentation bleeds into the output. Either you have clean code and ugly HTML, or the other way around.

Unacceptable.

With Feature::Dedent, this dilemma disappears:

$latte->setFeature(Latte\Feature::Dedent);

Latte automatically removes the common indentation inside paired tags. So this:

<ul>
    {foreach $items as $item}
        <li>{$item}</li>
    {/foreach}
</ul>

generates:

<ul>
    <li>...</li>
    <li>...</li>
</ul>

The indentation is gone as if it was never there.

And since dedent is performed at template compilation time, not during each render, it has zero performance impact. It works for all paired tags – {if}, {block}, {capture}, {foreach} and more. Nested tags are dedented independently, each at its own level.

If the indentation is inconsistent – for instance, you mix tabs with spaces, or a line doesn't have sufficient indentation – Latte throws a CompileException with the exact line number. No silent swallowing of errors.

Filter |commas

Joining an array into a comma-separated string – easy. But what if you want “and” instead of a comma before the last element? That's exactly the kind of thing where you start writing conditions in the template and suddenly you have four lines of code instead of one. And all you wanted to say was “apple, pear and plum”.

{['apple', 'pear', 'plum']|commas}        {* apple, pear, plum *}
{['apple', 'pear', 'plum']|commas:' and '} {* apple, pear and plum *}
{['apple', 'pear', 'plum']|commas:', or '} {* apple, pear, or plum *}

Without a parameter, it joins with a comma and space. With a parameter, it uses the given string as the separator between the last two elements – the rest stays comma-separated. Natural language, not foreach.

Filter |column

You have an array of user records, say [['id' => 1, 'name' => 'John'], ['id' => 2, 'name' => 'Jane'], ...], and you need just the names? The |column filter extracts values from a single column – whether it's an array key or an object property:

{$users|column:'name'|commas}   {* John, Jane, Bob *}

It optionally accepts a second parameter for indexing the results:

{foreach ($users|column:'name':'id') as $id => $name}
    {$id}: {$name}
{/foreach}

It works with iterators too, not just arrays – however, the iterator is internally converted to an array, so don't expect any lazy magic.

Filter |slice for Iterators and New Filter |limit

The |slice filter extracts a portion of an array or string (with full UTF-8 support for strings). Until now, it only worked with arrays and strings. Now it also handles iterators and generators – it returns a generator that reads elements from the source one by one and stops once the limit is reached. The entire iterator is never loaded into memory:

{foreach ($generator|slice:0:10) as $item}
    {$item}
{/foreach}

On top of that, there's a new filter |limit – a more convenient variant for the typical case of “take the first N elements”. It works with arrays, iterators, and strings (with UTF-8 support):

{foreach ($items|limit:5) as $item}
    {$item}
{/foreach}

{$description|limit:100}

The difference from |slice is that |limit preserves the original keys by default.

How to Enable

The new features ScopedLoopVariables and Dedent are enabled via setFeature():

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::ScopedLoopVariables);
$latte->setFeature(Latte\Feature::Dedent);

And if you're using Nette, just enable them in the configuration:

latte:
    scopedLoopVariables: true
    dedent: true

Five small things for life.

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.