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"
usen: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
andn: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(@Nette\Localization\Translator)
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;
}
}