Migrace z Latte v2 na v3

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, viz dokumentace.

Pokud používáte 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 verze 3.1 a také Texy již aktualizovány jsou a fungují jak s Latte 2, tak i novou verzí 3.

Inicializace se v Latte 3 zjednodušuje, vše obstará jediná metoda addExtension(), která přidá značky, filtry, providery, zkrátka všechno. Takže pokud registrujete rozšíření pro uvedené knihovny manuálně, aktualizujte je takto:

nette/application

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.

nette/forms

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

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

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

Jste autorem doplňků?

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' => [$this, 'createFoo'],
		];
	}

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

// 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í objekt Latte\Compiler\Node (ten pak je součástí AST stromu) a teprve Node má metodu print(Latte\Compiler\PrintContext $content): string která vrací PHP kód:

Mezikrok s vytvořením objektu Node lze zkrátit tím, že vrátíme předpřipravenou AuxiliaryNode, kde funkčnost vypsání PHP kódu předáme do konstruktoru:

// 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 ...')
	);
}

A nakonec, maska v $context->format() už nemá zkratky %node.***, předpokládá se, že obsah značky nejprve naparsujete. Takže nejprve využijeme parser a naparsujeme obsah do proměnných, a poté vypíšeme. Celý kód funkce by vypadal takto:

public function createFoo(Latte\Compiler\Tag $tag): Latte\Compiler\Node
{
	// parsování obsahu značky
	$subject = $tag->parser->parseUnquotedStringOrExpression();
	$tag->parser->stream->tryConsume(',');
	$args = $tag->parser->parseArguments();

	// vrácení Node, která vygeneruje PHP kód
	return new Latte\Compiler\Nodes\AuxiliaryNode(
		fn(Latte\Compiler\PrintContext $context) =>
			$context->format('echo %escape(MyClass:myFunc(%node, %node));', $subject, $args)
	);
}