Migrace z Latte 2 na 3

Latte 3 má kompletně přepsaným kompilátor a formálně přesně definovanou gramatiku. Ta by měla co nejvíce odpovídat Latte 2, ale existují konstrukce, které je třeba drobně upravit.

V praxi se ukazuje, že naprostou většinu šablon není potřeba nijak upravovat a fungují stejně v Latte 2 jako ve verzi 3. Jak ale odhalit nekompatibility?

Nejprve si nainstalujte přechodovou verzi Latte 2.11.

Tato verze nepřináší žádné novinky, jen pomocí E_USER_DEPRECATED upozorňuje na případy, u kterých ví, že je nové Latte nebude podporovat, a hlavně poradí, jak je upravit. Projít všechny šablony a otestovat, jestli jsou kompatibilní, vám pomůže nástroj Linter, který spustíte z konzole:

vendor/bin/latte-lint <cesta>

Když vyřešíte možné nekompatibility, aktualizujte na Latte 3.0. A opět pusťte Linter, abyste se ujistili, že nový striktní parser všem šablonám opravdu rozumí.

Změny v API

Změny v API se týkají jen přidávaní vlastních značek. Zbytek API zůstává stejný jako u verze 2, tj. stejným způsobem se vykreslují šablony, předávají parametry, registrují filtry.

Výjimkou je nahrazení tzv. dynamického filtru Engine::addFilter(null, ...) za zavaděč filtrů, který si liší tím, že vrací vždy callable a registruje se metodou Engine::addFilterLoader().

API pro přidávání vlastních značek je úplně jiné, takže doplňky určené pro Latte 2 s ním nebudou fungovat. Dále viz aktualizace doplňků.

Změny syntaxe

Změny jsou následující:

  • ve filtrech se jako oddělovač parametrů používá čárka, dříve |filtr: arg : arg je nyní |filtr: arg, arg
  • značka {label foo}...{/label} je vždy párová, nepárovou je potřeba psát {label /}
  • naopak značka {_'text'} je vždy nepárová, párovou {_}...{/} nahrazuje nové {translate}...{/translate}
  • pseudořetězce jako {block foo-$var} je potřeba psát v uvozovkách {block "foo-$var"} nebo doplnit složené závorky {block foo-{$var}}
  • to se týká i atributů, tj. místo n:block="foo-$var" použijte n:block="foo-{$var}".
  • je třeba dodržet velikost písmenek u filtrů, které jsou v Latte 3 case sensitive
  • značka {do ...} nebo {php ...} může obsahovat pouze výrazy, pro použití libovolného PHP zaregistrujte RawPhpExtension.

A ještě okrajové případy:

  • atributy n:inner-xxx, n:tag-xxx a n:ifcontent nelze použít u nepárových HTML elementů
  • atribut n:inner-snippet musí být psán bez inner-
  • musí být ukončené značky </script> a </style>
  • odstranění magické proměnné $iterations (neplést s $iterator!)
  • značku {includeblock file.latte} nahrazuje {include file.latte with blocks} nebo {import}
  • {include "abc"} by mělo být psáno jako {include file "abc"}, pokud "abc" neobsahuje tečku a není tak jasné, že jde o soubor

Aktualizace doplňků

S kompletním přepsáním parseru se zcela změnil i způsob psaní vlastních značek. Pokud máte pro Latte vytvořené vlastní značky, bude třeba je napsat znovu pro verzi 3, viz dokumentace.

Pokud používáte cizí doplněk, který přidává značky, je potřeba počkat, až autor vydá verzi pro Latte 3. Knihovny nette/application, nette/caching a nette/forms ve verzi 3.1 a také Texy již aktualizovány jsou a fungují jak s Latte 2, tak i 3.

nette/application

Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit.

Starý kód pro Latte 2:

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

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

Nový kód pro Latte 3:

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

UIExtension přidává značky n:href, {link}, {control}, {snippet}, atd. Značky pro snippety se tak přesouvají ze samotného Latte do knihovny nette/application. V Latte 3 se už nevolá metoda presenteru templatePrepareFilters().

nette/forms

Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit.

Starý kód pro Latte 2:

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

Nový kód pro Latte 3:

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

nette/caching

Při obvyklém použití Nette se toto rozšíření nastavuje automaticky a není třeba nic měnit.

Starý kód pro Latte 2:

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

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

Nový kód pro Latte 3:

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

Tracy

Panel pro Tracy se nyní aktivuje také jako rozšíření.

Starý kód pro Latte 2:

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

Nový kód pro Latte 3:

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

Překlady

TranslatorExtension přidává značky pro překlad {_'text'}, nové párové {translate}...{/translate} a filtr |translate.

Starý kód pro Latte 2:

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

Nový kód pro Latte 3:

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

V presenterech se automaticky aktivuje tím, že nastavíte šabloně translator metodou $template->setTranslator($translator). Bez toho značky pro překlad nebudou k dispozici a je potřeba rozšíření zaregistrovat ručně, nebo pomocí konfiguračního souboru, viz dále.

Konfigurační soubor

V Latte 2 bylo možné registrovat nové tagy pomocí konfiguračního souboru v sekci latte › macros. Ve verzi 3 se přidávají rozšíření tímto způsobem:

latte:
	extensions:
		- App\Templating\LatteExtension
		- Latte\Essential\TranslatorExtension(@Nette\Localization\Translator)

Vyvíjíte doplněk pro Latte?

Můžete ve své knihovně mít současně podporu obou verzí Latte. K detekci verze je nejlepší použít konstantu Latte\Engine::VERSION a oddělit tak použití onCompile[] a addMacro() od nového addExtension():

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

Jako příklad zkusíme přepsat následující kód určený pro Latte 2 do podoby pro Latte 3:

// starý kód pro 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 se rozšiřuje pomocí tzv. extensions. Triviální extension přidávající značku foo by vypadalo takto:

// nový kód pro Latte 3
class FooExtension extends Latte\Extension
{
	public function getTags(): array
	{
		return [
			'foo' => [FooNode::class, 'create'], // třídu FooNode doplníme za chvíli
		];
	}
}

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

Nový kompilátor je robustnější, neobsahuje dřívější zkratky, takže napsat makro vyžaduje o něco víc řádků kódu. Nelze například předat přímo řetězec s PHP kódem jako v Latte 2, místo toho vytvoříme funkci. Připomeňme, že v Latte 2 by ona funkce vypadala nějak takto:

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

Přesto na to Latte 3 jde v podstatě podobně, jen MacroNode se jmenuje Latte\Compiler\Tag a PhpWriter je Latte\Compiler\PrintContext. Ale především je tam navíc mezikrok, tedy že funkce nevrací přímo PHP kód, ale vrací uzel, tj. potomka StatementNode, který je pak součástí AST stromu. A tento uzel má metodu print(Latte\Compiler\PrintContext $content): string, která vrací PHP kód:

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

	public function print(Latte\Compiler\PrintContext $context): string
	{
		return $context->format('echo ...'); // vrací PHP kód
	}
}

Dále maska v $context->format() už nemá zkratky %node.***, předpokládá se, že obsah značky nejprve naparsujete. Takže využijeme parser a naparsujeme obsah do proměnných (poduzlů), a poté vypíšeme:

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;
		// parsování obsahu značky
		$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,
		);
	}
}

A nakonec doplníme metodu getIterator(), aby bylo možné poduzly procházet při tzv. traversování:

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

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