Наследование и переиспользование шаблонов

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

Концепция наследования шаблонов Latte похожа на наследование классов в PHP. Вы определяете родительский шаблон, от которого могут наследоваться другие дочерние шаблоны, переопределяя части родительского шаблона. Это отлично работает, когда у элементов есть общая структура. Звучит сложно? Не переживайте, это очень просто.

Наследование макета {layout}

Рассмотрим наследование макета на примере. Вот родительский шаблон, назовём его layout.latte, который задаёт каркас HTML-документа:

<!doctype html>
<html lang="en">
<head>
	<title>{block title}{/block}</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		{block content}{/block}
	</div>
	<div id="footer">
		{block footer}&copy; Copyright 2008{/block}
	</div>
</body>
</html>

Теги {block} задают три блока, которые могут заполнить дочерние шаблоны. Всё, что делает тег блока, – сообщает движку шаблонов, что дочерний шаблон может переопределить эти части, определив собственный блок с тем же именем.

Дочерний шаблон может выглядеть так:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

{block content}
	<p>Welcome to my awesome homepage.</p>
{/block}

Ключевую роль здесь играет тег {layout}. Он говорит Latte, что этот шаблон “расширяет” другой шаблон. Отрисовывая этот шаблон, Latte сначала находит родительский, в данном случае layout.latte.

В этот момент Latte замечает три тега блоков в layout.latte и заменяет эти блоки содержимым дочернего шаблона. Поскольку дочерний шаблон не определил блок footer, вместо него используется содержимое из родительского шаблона. Содержимое внутри тега {block} в родительском шаблоне всегда служит запасным вариантом.

Вывод может выглядеть так:

<!doctype html>
<html lang="en">
<head>
	<title>My amazing blog</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		<p>Welcome to my awesome homepage.</p>
	</div>
	<div id="footer">
		&copy; Copyright 2008
	</div>
</body>
</html>

В дочернем шаблоне блоки обычно размещаются на верхнем уровне или внутри другого блока, например:

{block content}
	<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}

Кроме того, блок создаётся всегда, независимо от того, вычисляется ли окружающее условие {if} как истинное или ложное. Так что, хотя по виду и не скажешь, этот шаблон блок всё-таки определяет.

{if false}
	{block head}
		<meta name="robots" content="noindex, follow">
	{/block}
{/if}

Если вы хотите, чтобы вывод внутри блока отображался по условию, используйте вместо этого следующее:

{block head}
	{if $condition}
		<meta name="robots" content="noindex, follow">
	{/if}
{/block}

Код в шапке дочернего шаблона (то есть до первого блока или любого вывода) выполняется до отрисовки шаблона макета, поэтому вы можете использовать его для определения переменных вроде {var $foo = bar} и передавать данные по всей цепочке наследования. Код, размещённый между блоками или после них в шаблоне с {layout}, не выполняется вообще:

{layout 'layout.latte'}
{var $robots = noindex}

...

Если вы хотите передать переменные только в макет, не создавая их в текущем шаблоне, перечислите их прямо в теге {layout} (или {extends}) после запятой:

{layout 'layout.latte', robots: noindex}

Переменная $robots будет доступна в макете и его блоках, но не в блоках текущего шаблона. Явно переданная переменная также имеет приоритет над одноимённым параметром шаблона.

Многоуровневое наследование

Вы можете использовать сколько угодно уровней наследования. Один из распространённых способов применения наследования макетов – следующий трёхуровневый подход:

  1. Создайте шаблон layout.latte, который держит общий облик вашего сайта.
  2. Создайте шаблон layout-SECTIONNAME.latte для каждого раздела сайта. Например, layout-news.latte, layout-blog.latte и так далее. Все эти шаблоны расширяют layout.latte и содержат стили и оформление, специфичные для каждого раздела.
  3. Создайте отдельные шаблоны для каждого типа страницы, например для новостной заметки или записи блога. Эти шаблоны расширяют соответствующий шаблон раздела.

Динамическое наследование макета

В качестве имени родительского шаблона можно использовать переменную или любое выражение PHP, так что наследование может вести себя динамически:

{layout $standalone ? 'minimum.latte' : 'layout.latte'}

Вы можете также использовать API Latte, чтобы выбирать шаблон макета автоматически.

Советы

Вот несколько советов по работе с наследованием макетов:

  • Если вы используете {layout} в шаблоне, он должен стоять в шапке шаблона, то есть до любого вывода. Перед ним могут стоять только теги, не производящие вывода (такие как {var}, {templateType}, {import} или комментарии).
  • Макет может находиться автоматически (как в презентерах). В этом случае, если у шаблона не должно быть макета, он указывает это тегом {layout none}. И наоборот, {layout auto} (или {extends auto}) возвращает автоматический поиск макета.
  • У тега {layout} есть псевдоним {extends}.
  • Имя файла макета зависит от загрузчика.
  • Блоков может быть сколько угодно. Помните, что дочерние шаблоны не обязаны определять все родительские блоки, поэтому вы можете заполнить разумными значениями по умолчанию несколько блоков, а позже определять только те, которые нужны.

Блоки {block}

См. также анонимный {block}

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

{foreach $posts as $post}
{block post}
	<h1>{$post->title}</h1>
	<p>{$post->body}</p>
{/block}
{/foreach}

Если вы отрисуете этот шаблон, результат будет ровно таким же и с тегами {block}, и без них. Блоки имеют доступ к переменным из внешних областей видимости. Они лишь дают возможность переопределить себя в дочернем шаблоне:

{layout 'parent.latte'}

{block post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/block}

Теперь при отрисовке дочернего шаблона цикл будет использовать блок, определённый в дочернем шаблоне child.latte, вместо того, что определён в parent.latte; выполняемый шаблон тогда равнозначен следующему:

{foreach $posts as $post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/foreach}

Однако если мы создадим внутри именованного блока новую переменную или заменим значение существующей, изменение будет видно только внутри блока:

{var $foo = 'foo'}
{block post}
	{do $foo = 'new value'}
	{var $bar = 'bar'}
{/block}

foo: {$foo}                  // выводит: foo
bar: {$bar ?? 'not defined'} // выводит: not defined

Содержимое блока можно изменить фильтрами. Следующий пример убирает весь HTML и переводит текст в верхний регистр:

<title>{block title|stripHtml|capitalize}...{/block}</title>

Тег можно записать и как n:атрибут:

<article n:block=post>
	...
</article>

Локальные блоки

Каждый блок переопределяет содержимое одноимённого родительского блока, за исключением локальных блоков. Они подобны приватным методам классов. Вы можете создать шаблон, не опасаясь, что из-за случайного совпадения имён блоков их перезапишет другой шаблон.

{block local helper}
	...
{/block}

Вывод блоков {include}

См. также {include file}

Чтобы вывести блок в определённом месте, используйте тег {include blockname}:

<title>{block title}{/block}</title>

<h1>{include title}</h1>

Вы можете вывести и блок из другого шаблона:

{include footer from 'main.latte'}

Отрисовываемый блок не имеет доступа к переменным активного контекста, если только блок не определён в том же файле, где он подключается. Зато он имеет доступ к глобальным переменным.

Передать переменные в блок можно так:

{include footer, foo: bar, id: 123}

Имя блока может быть переменной или любым выражением PHP. В этом случае добавьте перед переменной ключевое слово block, чтобы Latte во время компиляции знал, что это блок, а не подключаемый шаблон, имя которого тоже могло бы быть в переменной:

{var $name = footer}
{include block $name}

Блок можно отрисовать и внутри самого себя, что полезно, например, при отрисовке древовидной структуры:

{define menu, $items}
<ul>
	{foreach $items as $item}
		<li>
		{if is_array($item)}
			{include menu, $item}
		{else}
			{$item}
		{/if}
		</li>
	{/foreach}
</ul>
{/define}

Вместо {include menu, ...} мы можем написать и {include this, ...}, где this означает текущий блок.

Отрисованное содержимое блока можно изменить фильтрами. Следующий пример убирает весь HTML и переводит текст в верхний регистр:

<title>{include heading|stripHtml|capitalize}</title>

Родительский блок

Если вам нужно вывести содержимое блока из родительского шаблона, используйте {include parent}. Это полезно, когда вы хотите дополнить содержимое родительского блока, а не полностью его переопределить.

{block footer}
	{include parent}
	<a href="https://github.com/nette">GitHub</a>
	<a href="https://twitter.com/nettefw">Twitter</a>
{/block}

Определения {define}

Помимо блоков в Latte есть и “определения”. В обычных языках программирования их можно сравнить с функциями. Они полезны для переиспользования фрагментов шаблона, чтобы избежать повторов.

Latte старается держать всё простым, поэтому по сути определения – то же самое, что блоки, и всё сказанное о блоках относится и к определениям. От блоков они отличаются тем, что:

  1. заключены в теги {define}
  2. отрисовываются только при вставке через {include}
  3. для них можно определить параметры, как для функций в PHP
{block foo}<p>Hello</p>{/block}
{* выводит: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* не выводит ничего *}

{include bar}
{* выводит: <p>World</p> *}

Представьте, что у вас есть вспомогательный шаблон с набором определений того, как рисовать HTML-формы.

{define input, $name, $value, $type = 'text'}
	<input type={$type} name={$name} value={$value}>
{/define}

{define textarea, $name, $value}
	<textarea name={$name}>{$value}</textarea>
{/define}

Аргументы всегда необязательны и по умолчанию равны null, если только значение по умолчанию не указано (здесь 'text' – значение по умолчанию для $type). Можно объявить и типы параметров: {define input, string $name, ...}.

Шаблон с определениями загружается через {import}. Сами определения отрисовываются так же, как блоки:

<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>

Как и блоки, определения не имеют доступа к переменным активного контекста, только к глобальным переменным. Исключение составляет определение без объявленных параметров, к которому обращаются по статическому имени и которое подключается в том же файле, где определено: такое определение имеет доступ к переменным контекста того места, откуда оно подключается.

Динамические имена блоков

Latte даёт большую гибкость в определении блоков, потому что имя блока может быть любым выражением PHP. Этот пример определяет три блока с именами hi-Peter, hi-John и hi-Mary:

{foreach [Peter, John, Mary] as $name}
	{block "hi-$name"}Hi, I am {$name}.{/block}
{/foreach}

В дочернем шаблоне мы затем можем переопределить, например, только один блок:

{block hi-John}Hello. I am {$name}.{/block}

Так что вывод будет выглядеть так:

Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.

Проверка существования блока {ifset}

См. также {ifset $var}

Используйте проверку {ifset blockname}, чтобы узнать, существует ли блок (или несколько блоков) в текущем контексте:

{ifset footer}
	...
{/ifset}

{ifset footer, header, main}
	...
{/ifset}

Имя блока может быть переменной или любым выражением PHP. В этом случае добавьте перед переменной ключевое слово block, чтобы уточнить, что это не проверка существования переменных:

{ifset block $name}
	...
{/ifset}

Существование блоков проверяет и функция hasBlock():

{if hasBlock(header) || hasBlock(footer)}
	...
{/if}

Советы

Несколько советов по работе с блоками:

  • Последнему блоку верхнего уровня закрывающий тег не нужен (блок заканчивается вместе с концом документа). Это упрощает написание дочерних шаблонов, содержащих один основной блок.
  • Ради читаемости вы можете при желании указать имя блока и в теге {/block}, например {/block footer}. Но имя должно совпадать с именем блока. В крупных шаблонах этот приём помогает видеть, какие теги блоков закрываются.
  • Нельзя напрямую определить в одном шаблоне несколько тегов блоков с одинаковым именем. Но этого можно добиться с помощью Динамические имена блоков.
  • Для определения блоков можно использовать n:атрибуты, например <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Блоки можно использовать и без имён, только чтобы применить к выводу фильтры{block|strip} hello {/block}

Горизонтальное переиспользование {import}

Горизонтальное переиспользование – третий механизм переиспользования и наследования в Latte. Оно позволяет загружать блоки из других шаблонов. Это похоже на создание файла со вспомогательными функциями в PHP и его последующую загрузку через require.

Наследование макетов – одна из самых мощных возможностей Latte, но оно ограничено простым наследованием: шаблон может расширять только один другой шаблон. Горизонтальное переиспользование – способ добиться множественного наследования.

Пусть у нас есть файл с определениями блоков:

{block sidebar}...{/block}

{block menu}...{/block}

С помощью команды {import} мы импортируем все блоки и Определения, определённые в blocks.latte, в другой шаблон:

{import 'blocks.latte'}

{* теперь можно использовать блоки sidebar и menu *}

Если вы импортируете блоки в родительском шаблоне (то есть используете {import} в layout.latte), блоки будут доступны и во всех дочерних шаблонах, что очень удобно.

Шаблон, предназначенный для импорта (например, blocks.latte), не должен расширять другой шаблон, то есть использовать {layout}. Зато он может импортировать другие шаблоны.

Тег {import} должен быть первым тегом шаблона после {layout}. Имя шаблона может быть любым выражением PHP:

{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}

Вы можете использовать в шаблоне сколько угодно инструкций {import}. Если два импортированных шаблона определяют один и тот же блок, побеждает первый. Однако у главного шаблона наивысший приоритет, и он может переопределить любой импортированный блок.

Тег {import} может также передать аргументы в импортируемый шаблон, например {import 'blocks.latte', foo: 1}. Эти аргументы затем доступны как переменные в импортированных блоках и определениях.

Содержимое перезаписанных блоков можно сохранить, вставив блок так же, как Родительский блок:

{layout 'layout.latte'}

{import 'blocks.latte'}

{block sidebar}
	{include parent}
{/block}

{block title}...{/block}
{block content}...{/block}

В этом примере {include parent} вызывает блок sidebar из шаблона blocks.latte.

Блочное наследование {embed}

Блочное наследование распространяет идею наследования макетов на уровень фрагментов содержимого. Наследование макетов работает с “каркасами документов”, которые оживляют дочерние шаблоны, а блочное наследование позволяет создавать каркасы для меньших единиц содержимого и переиспользовать их где угодно.

В блочном наследовании ключевую роль играет тег {embed}. Он объединяет поведение {include} и {layout}. Он позволяет встроить содержимое другого шаблона или блока и при желании передать переменные, как {include}. Он также позволяет переопределить любой блок, определённый внутри встраиваемого шаблона, как {layout}.

Для примера возьмём элемент “аккордеон”. Взгляните на каркас элемента, хранящийся в шаблоне collapsible.latte:

<section class="collapsible {$modifierClass}">
	<h4 class="collapsible__title">
		{block title}{/block}
	</h4>

	<div class="collapsible__content">
		{block content}{/block}
	</div>
</section>

Теги {block} задают два блока, которые могут заполнить дочерние шаблоны. Да, ровно как в случае родительского шаблона при наследовании макетов. Вы также видите переменную $modifierClass.

Используем наш элемент в шаблоне. Вот здесь и появляется {embed}. Это исключительно мощный тег, который позволяет нам сделать всё сразу: встроить содержимое шаблона элемента, добавить в него переменные и добавить в него блоки с собственным HTML:

{embed 'collapsible.latte', modifierClass: my-style}
	{block title}
		Hello World
	{/block}

	{block content}
		<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
		elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
	{/block}
{/embed}

Вывод может выглядеть так:

<section class="collapsible my-style">
	<h4 class="collapsible__title">
		Hello World
	</h4>

	<div class="collapsible__content">
		<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
		elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
	</div>
</section>

Блоки внутри тегов embed образуют отдельный слой, изолированный от блоков вне embed. Поэтому они могут называться так же, как блок снаружи, не сталкиваясь с ним, и он на них не влияет. С помощью тега include внутри тегов {embed} вы можете вставить блоки, созданные здесь, блоки встраиваемого шаблона (которые не являются локальными), а также блоки главного шаблона, которые локальными являются. Вы можете также импортировать блоки из других файлов:

{block outer}…{/block}
{block local hello}…{/block}

{embed 'collapsible.latte', modifierClass: my-style}
	{import 'blocks.latte'}

	{block inner}…{/block}

	{block title}
		{include inner} {* работает, блок определён внутри embed *}
		{include hello} {* работает, блок локальный в этом шаблоне *}
		{include content} {* работает, блок определён во встраиваемом шаблоне *}
		{include aBlockDefinedInImportedTemplate} {* работает *}
		{include outer} {* не работает! - блок во внешнем слое *}
	{/block}
{/embed}

Встраиваемые шаблоны не имеют доступа к переменным активного контекста, но имеют доступ к глобальным переменным.

С помощью {embed} можно встраивать не только шаблоны, но и другие блоки, поэтому предыдущий пример можно было записать так:

{define collapsible}
<section class="collapsible {$modifierClass}">
	<h4 class="collapsible__title">
		{block title}{/block}
	</h4>
	...
</section>
{/define}


{embed collapsible, modifierClass: my-style}
	{block title}
		Hello World
	{/block}
	...
{/embed}

Между этими двумя вариантами, однако, есть одно отличие: когда вы встраиваете блок, а не файл, блоки из внешнего слоя остаются доступны внутри embed. Так что, в отличие от встроенного файла, {include outer} там сработал бы.

Если мы передаём в {embed} выражение и неясно, имя это блока или файла, добавьте ключевое слово block или file:

{embed block $name} ... {/embed}

Сценарии использования

В Latte есть разные виды наследования и переиспользования кода. Для наглядности обобщим главные концепции:

{include template}

Сценарий: использование header.latte и footer.latte внутри layout.latte.

header.latte

<nav>
   <div>Home</div>
   <div>About</div>
</nav>

footer.latte

<footer>
   <div>Copyright</div>
</footer>

layout.latte

{include 'header.latte'}

<main>{block main}{/block}</main>

{include 'footer.latte'}

{layout}

Сценарий: расширение layout.latte в homepage.latte и about.latte.

layout.latte

{include 'header.latte'}

<main>{block main}{/block}</main>

{include 'footer.latte'}

homepage.latte

{layout 'layout.latte'}

{block main}
	<p>Homepage</p>
{/block}

about.latte

{layout 'layout.latte'}

{block main}
	<p>About page</p>
{/block}

{import}

Сценарий: использование sidebar.latte в single.product.latte и single.service.latte.

sidebar.latte

{block sidebar}<aside>This is sidebar</aside>{/block}

single.product.latte

{layout 'product.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Product page</main>{/block}

single.service.latte

{layout 'service.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Service page</main>{/block}

{define}

Сценарий: функции, которые принимают переменные и что-то отрисовывают.

form.latte

{define form-input, $name, $value, $type = 'text'}
	<input type={$type} name={$name} value={$value}>
{/define}

profile.service.latte

{import 'form.latte'}

<form action="" method="post">
	<div>{include form-input, username}</div>
	<div>{include form-input, password}</div>
	<div>{include form-input, submit, Submit, submit}</div>
</form>

{embed}

Сценарий: встраивание pagination.latte в product.table.latte и service.table.latte.

pagination.latte

<div id="pagination">
	<div>{block first}{/block}</div>

	{for $i = $min + 1; $i < $max - 1; $i++}
		<div>{$i}</div>
	{/for}

	<div>{block last}{/block}</div>
</div>

product.table.latte

{embed 'pagination.latte', min: 1, max: $products->count}
	{block first}First Product Page{/block}
	{block last}Last Product Page{/block}
{/embed}

service.table.latte

{embed 'pagination.latte', min: 1, max: $services->count}
	{block first}First Service Page{/block}
	{block last}Last Service Page{/block}
{/embed}
версия: 3.x