Migration from Latte 2 to 3

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 version 3, see documentation.

If you are using a foreign 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 in version 3.1, as well as Texy, have already been updated and work with both Latte 2 and 3.

nette/application

When using Nette normally, this extension is set automatically and there is no need to change anything.

Old code for Latte 2:

$latte->onCompile[] = function ($latte) {
	Nette\Bridges\ApplicationLatte\UIMacros::install($latte->getCompiler());
};

$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.

nette/forms

When using Nette normally, this extension is set automatically and there is no need to change anything.

Old code for Latte 2:

$latte->onCompile[] = function ($latte) {
	Nette\Bridges\FormsLatte\FormMacros::install($latte->getCompiler());
};

New code for Latte 3:

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

nette/caching

When using Nette normally, this extension is set automatically and there is no need to change anything.

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));

Tracy

The panel for Tracy is now also activated as an extension.

Old code for Latte 2:

$latte = new Latte\Engine;
Latte\Bridges\Tracy\LattePanel::initialize($latte);

New code for Latte 3:

$latte = new Latte\Engine;
$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension);

Translations

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 or with a configuration file.

Configuration File

In Latte 2 it was possible to register new tags using config file in the latte › macros section. In version 3, entire extensions are added this way:

latte:
	extensions:
		- App\Templating\LatteExtension
		- Latte\Essential\TranslatorExtension

Do You Develop Add-on for Latte?

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' => [FooNode::class, 'create'], // we will add the FooNode class in a moment
		];
	}
}

// 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 goes about it in much the same way, only MacroNode is called Latte\Compiler\Tag and PhpWriter is Latte\Compiler\PrintContext. But most importantly, there is an extra intermediate step, which is that the function does not return PHP code directly, but returns a node, i.e. a child of StatementNode, which is then part of the AST tree. And this node has a method print(Latte\Compiler\PrintContext $content): string that returns PHP code:

// Latte 3
class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		return $context->format('echo ...'); // returns PHP code
	}
}

Furthermore, the mask in $context->format() no longer has %node.*** abbreviations, it is assumed that you parse the tag content first. So we use the parser to parse the content into variables (subnodes), and then we write it out:

use Latte\Compiler\Nodes\Php\Expression\ArrayNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;

class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	public ExpressionNode $subject;
	public ArrayNode $args;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = new self;
		// parsing the content of the tag
		$node->subject = $tag->parser->parseUnquotedStringOrExpression();
		$tag->parser->stream->tryConsume(',');
		$node->args = $tag->parser->parseArguments();
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		return $context->format(
			'echo %escape(MyClass:myFunc(%node, %node));',
			$this->subject,
			$this->args,
		);
	}
}

Finally, we will add the getIterator() method to allow subnodes to be traversed when traversing:

class FooNode extends Latte\Compiler\Nodes\StatementNode
{
	...

	public function &getIterator(): \Generator
	{
		yield $this->subject;
		yield $this->args;
	}
}
version: 3.0