Миграция с Latte 2 на 3

У Latte 3 полностью переписанный компилятор и формально строго заданная грамматика. Она должна как можно точнее соответствовать Latte 2, но некоторые конструкции требуют небольшой правки.

На практике оказывается, что подавляющее большинство шаблонов не нуждается ни в каких изменениях и работает в Latte 3 так же, как в Latte 2. Но как обнаружить несовместимости?

Сначала установите переходную версию Latte 2.11.

Эта версия не приносит новых возможностей, она лишь выдаёт предупреждение через E_USER_DEPRECATED для случаев, о которых знает, что новый Latte их не поддержит, а главное – подсказывает, как их исправить. Чтобы пройти по всем шаблонам и проверить их совместимость, можно воспользоваться инструментом Linter, который запускается из консоли:

vendor/bin/latte-lint <path>

Устранив возможные несовместимости, обновитесь до Latte 3.0. И запустите Linter снова, чтобы убедиться, что новый строгий парсер действительно понимает все шаблоны.

Изменения API

Изменения API касаются только добавления пользовательских тегов. Остальной API остался таким же, как в версии 2, то есть отрисовка шаблонов, передача параметров и регистрация фильтров работают по-прежнему.

Исключение составляет так называемый динамический фильтр Engine::addFilter(null, ...), о котором теперь заботятся фильтры, зарегистрированные через класс с помощью метода addFilter(). Исходный метод Engine::addFilterLoader() пока существует как переходное решение, но объявлен устаревшим.

API для добавления пользовательских тегов совершенно другой, поэтому дополнения, рассчитанные на Latte 2, с ним работать не будут. См. также Обновление дополнений.

Изменения синтаксиса

Изменения таковы:

  • фильтры используют запятую как разделитель параметров: раньше |filter: arg : arg, теперь |filter: arg, arg
  • тег {label foo}...{/label} всегда парный, непарный нужно писать {label /}
  • зато тег {_'text'} всегда непарный, а парный {_}...{/} заменён новым {translate}...{/translate}
  • псевдострочки вроде {block foo-$var} нужно писать в кавычках {block "foo-$var"} или добавлять фигурные скобки {block foo-{$var}}
  • то же относится к атрибутам, то есть вместо n:block="foo-$var" используйте n:block="foo-{$var}"
  • в Latte 3 необходимо соблюдать регистр в именах фильтров
  • тег {do ...} или {php ...} может содержать только выражения; чтобы использовать произвольный PHP, зарегистрируйте RawPhpExtension.

И ещё несколько пограничных случаев:

  • атрибуты n:inner-xxx, n:tag-xxx и n:ifcontent нельзя использовать на пустых (void) HTML-элементах
  • атрибут n:inner-snippet нужно писать без inner-
  • теги </script> и </style> должны быть закрыты
  • магическая переменная $iterations удалена (не путайте с $iterator!)
  • замените тег {includeblock file.latte} на {include file.latte with blocks} или {import}
  • {include "abc"} следует писать как {include file "abc"}, если только "abc" не содержит точку и не очевидно, что это файл

Обновление дополнений

С полным переписыванием парсера способ написания пользовательских тегов полностью изменился. Если у вас есть свои теги для Latte, вам придётся переписать их для версии 3, см. документацию.

Если вы используете чужое дополнение, добавляющее теги, придётся подождать, пока автор выпустит версию для Latte 3. Библиотеки nette/application, nette/caching и nette/forms в версии 3.1, а также Texy уже обновлены и работают и с Latte 2, и с Latte 3.

nette/application

При обычном использовании Nette это расширение подключается автоматически, и менять ничего не нужно.

Старый код для Latte 2:

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

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

Новый код для Latte 3:

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

UIExtension добавляет n:href, {link}, {control}, {snippet} и так далее. Теги для сниппетов таким образом переехали из самого Latte в библиотеку nette/application. В Latte 3 метод презентера templatePrepareFilters() больше не вызывается.

nette/forms

При обычном использовании Nette это расширение подключается автоматически, и менять ничего не нужно.

Старый код для Latte 2:

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

Новый код для Latte 3:

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

nette/caching

При обычном использовании Nette это расширение подключается автоматически, и менять ничего не нужно.

Старый код для Latte 2:

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

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

Новый код для Latte 3:

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

Tracy

Панель для Tracy теперь тоже подключается как расширение.

Старый код для Latte 2:

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

Новый код для Latte 3:

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

Переводы

TranslatorExtension добавляет теги перевода {_'text'}, новый парный {translate}...{/translate} и фильтр |translate.

Старый код для Latte 2:

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

Новый код для Latte 3:

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

В презентерах оно подключается автоматически при установке переводчика в шаблон методом $template->setTranslator($translator). Без этого теги перевода будут недоступны, и вам нужно зарегистрировать расширение вручную или через конфигурационный файл.

Конфигурационный файл

В Latte 2 новые теги можно было регистрировать через конфигурационный файл в секции latte › macros. В версии 3 так добавляются целые расширения:

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

Разрабатываете дополнение для Latte?

Вы можете поддерживать обе версии Latte в своей библиотеке одновременно. Для определения версии лучше всего использовать константу Latte\Engine::VERSION, чтобы отделить использование onCompile[] и addMacro() от нового addExtension():

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

Для примера попробуем переписать следующий код, предназначенный для Latte 2, в форму для Latte 3:

// старый код для 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 расширяется с помощью расширений. Тривиальное расширение, добавляющее тег foo, выглядело бы так:

// новый код для Latte 3
class FooExtension extends Latte\Extension
{
	public function getTags(): array
	{
		return [
			'foo' => [FooNode::class, 'create'], // класс FooNode добавим через минуту
		];
	}
}

// регистрация
$this->latte->addExtension(new FooExtension);

Новый компилятор надёжнее, в нём нет прежних сокращений, поэтому запись макроса занимает чуть больше строк кода. Например, мы не можем напрямую передать строку с PHP-кодом, как в Latte 2, вместо этого мы создаём функцию. Напомним, что в Latte 2 функция выглядела бы примерно так:

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

Тем не менее Latte 3 подходит к этому очень похоже, только MacroNode называется Latte\Compiler\Tag, а PhpWriter – Latte\Compiler\PrintContext. Но главное, что появился дополнительный промежуточный шаг: функция возвращает не PHP-код напрямую, а узел, то есть потомка StatementNode, который затем становится частью дерева AST. И у этого узла есть метод print(Latte\Compiler\PrintContext $context): string, возвращающий PHP-код:

// 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 ...'); // возвращает PHP-код
	}
}

Кроме того, в маске $context->format() больше нет сокращений %node.***: предполагается, что вы сначала разберёте содержимое тега. Итак, мы разбираем содержимое парсером в переменные (подузлы), а затем выводим его:

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;
		// разбор содержимого тега
		$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,
		);
	}
}

Наконец, мы добавим метод getIterator(), чтобы подузлы можно было обходить при обходе дерева:

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

	public function &getIterator(): \Generator
	{
		yield $this->subject;
		yield $this->args;
	}
}
версия: 3.x