Migration from Latte v2 to v3

Latte 3 has a completely rewritten compiler and a formally well-defined grammar. This should match Latte 2 as closely as possible, but there are some constructs that need minor tweaking.

In practice, it turns out that the vast majority of the templates do not need any modification and work the same in Latte 2 as they do in Latte 3. But how to detect incompatibilities?

First, install the transition version Latte 2.11.

This version doesn't bring any new features, it just provides a warning using E_USER_DEPRECATED for cases it knows the new Latte won't support, and more importantly advises you how to fix them. To go through all the templates and test if they are compatible, you can use the Linter tool that you run from the console:

vendor/bin/latte-lint <path>

Once you have resolved possible incompatibilities, upgrade to Latte 3.0. And run Linter again to make sure the new strict parser really understands all templates.

API Changes

The API changes only apply to adding custom tags. The rest of the API remains the same as version 2, i.e. the same way to render templates, pass parameters, register filters.

The exception is the replacement of the so-called dynamic filter Engine::addFilter(null, ...) with filter loader, which differs in that it always returns callable and is registered with the Engine::addFilterLoader() method.

The API for adding custom tags is completely different, so add-ons designed for Latte 2 won't work with it. See also Updates to add-ons.

Syntax Changes

The changes are as follows:

  • filters use a comma as parameter separator, previously |filter: arg : arg is now |filter: arg, arg
  • the {label foo}...{/label} tag is always paired, unpaired should be written {label /}
  • on the other hand, the {_'text'} tag is always unpaired, the paired {_}...{/} is replaced by the new {translate}...{/translate}
  • pseudo-strings such as {block foo-$var} need to be written in quotes {block "foo-$var"} or add compound brackets {block foo-{$var}}
  • this also applies to attributes, i.e. instead of n:block="foo-$var" use n:block="foo-{$var}".
  • it is necessary to be case sensitive for filters in Latte 3
  • The {do ...} or {php ...} tag can only contain expressions, to use any PHP register RawPhpExtension.

And more edge cases:

  • the n:inner-xxx, n:tag-xxx and n:ifcontent attributes cannot be used on void HTML elements
  • the n:inner-snippet attribute must be written without inner-
  • the tags </script> and </style> must be terminated
  • the magic variable $iterations has been removed (not to be confused with $iterator!)
  • replace the {includeblock file.latte} tag with {include file.latte with blocks} or {import}
  • {include "abc"} should be written as {include file "abc"} unless "abc" contains a period and it is clear that it is a file

Updates to Add-Ons

With the complete rewrite of the parser, the way to write custom tags has completely changed. If you have custom tags created for Latte, you will need to re-write them for the new version, see documentation.

If you use an add-on that adds tags, you will need to wait until the author releases a version for Latte 3. The nette/application, nette/caching and nette/forms libraries of version 3.1, as well as Texy, have already been updated and work with both Latte 2 and the new version 3.

Initialization is simplified in Latte 3, everything is handled by a single addExtension() method that adds tags, filters, providers, in short everything. So if you register extensions for the listed libraries manually, update them like this:


Old code for Latte 2:

$latte->onCompile[] = function ($latte) {

$latte->addProvider('uiControl', $control);
$latte->addProvider('uiPresenter', $control->getPresenter());

New code for Latte 3:

$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension($control));

UIExtension adds n:href, {link}, {control}, {snippet}, etc. The tags for snippets are thus moved from Latte itself to the nette/application library. In Latte 3, the presenter method templatePrepareFilters() is no longer called.


Old code for Latte 2:

$latte->onCompile[] = function ($latte) {

New code for Latte 3:

$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);


Old code for Latte 2:

$latte->onCompile[] = function ($latte) {
	$latte->getCompiler()->addMacro('cache', new Nette\Bridges\CacheLatte\CacheMacro);

$latte->addProvider('cacheStorage', $cacheStorage);

New code for Latte 3:

$latte->addExtension(new Nette\Bridges\CacheLatte\CacheExtension($cacheStorage));


TranslatorExtension adds the translation tags {_'text'}, new pair {translate}...{/translate} and the |translate filter.

Old code for Latte 2:

$latte->addFilter('translate', [$translator, 'translate']);

New code for Latte 3:

$latte->addExtension(new Latte\Essential\TranslatorExtension($translator));

In presenters, it is automatically activated by setting the translator to the template using the $template->setTranslator($translator) method. Without this, the translation tags will not be available and you need to register the extension manually.

Are You the Author of the Add-Ons?

You can have support for both versions of Latte in your library at the same time. To detect the version, it is best to use the Latte\Engine::VERSION constant to separate the use of onCompile[] and addMacro() from the new addExtension():

if (version_compare(Latte\Engine::VERSION, '3', '<')) {
	// Latte 2 initialization
	$this->latte->onCompile[] = function ($latte) {
		$latte->addMacro(/* ... */);
} else {
	// Latte 3 initialization
	$this->latte->addExtension(/* ... */);

As an example, let's try rewriting the following code intended for Latte 2 into a form for Latte 3:

// old code for Latte 2
$this->latte->onCompile[] = function (Latte\Engine $latte) {
	$set = new Latte\Macros\MacroSet($latte->getCompiler());
	$set->addMacro('foo', 'echo %escape(MyClass:myFunc(%node.word, %node.array))');

Latte 3 is extended using extensions. A trivial extension adding the foo tag would look like this:

// new code for Latte 3
class FooExtension extends Latte\Extension
	public function getTags(): array
		return [
			'foo' => [$this, 'createFoo'],

	public function createFoo(Latte\Compiler\Tag $tag): Latte\Compiler\Node
		// ...

// registration
$this->latte->addExtension(new FooExtension);

The new compiler is more robust, it doesn't include the previous shortcuts, so it takes a bit more lines of code to write a macro. For example, we can't directly pass a string of PHP code as in Latte 2, instead we create a function. Recall that in Latte 2 the function would look something like this:

// Latte 2
$set->addMacro('foo', function (Latte\MacroNode $node, Latte\PhpWriter $writer) {
	return $writer->write('echo %escape(MyClass:myFunc(%node.word, %node.array))');

Nevertheless, Latte 3 is basically similar, only MacroNode is called Latte\Compiler\Tag and PhpWriter is Latte\Compiler\PrintContext. But first of all there is an extra intermediate step, i.e. the function does not return PHP code directly, but returns a Latte\Compiler\Node object (which is then part of the AST tree) and only the Node has a method print(Latte\Compiler\PrintContext $content): string which returns PHP code:

The intermediate step of creating a Node object can be shortened by returning a prebuilt AuxiliaryNode, where the functionality of outputting PHP code is passed to the constructor:

// Latte 3
public function createFoo(Latte\Compiler\Tag $tag): Latte\Compiler\Node
	return new Latte\Compiler\Nodes\AuxiliaryNode(
		fn (Latte\Compiler\PrintContext $context) => $context->format('echo ...')

Finally, the mask in $context->format() no longer has %node.*** abbreviations, it is assumed that you parse the tag content first. So first we use the parser and parse the content into variables, and then we write it out. The whole code of the function would look like this:

public function createFoo(Latte\Compiler\Tag $tag): Latte\Compiler\Node
	// parsing the content of the tag
	$subject = $tag->parser->parseUnquotedStringOrExpression();
	$args = $tag->parser->parseArguments();

	// return Node that generates PHP code
	return new Latte\Compiler\Nodes\AuxiliaryNode(
		fn(Latte\Compiler\PrintContext $context) =>
			$context->format('echo %escape(MyClass:myFunc(%node, %node));', $subject, $args)