Наследование шаблонов и возможность повторного использования

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

Концепция наследования шаблонов 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} определяют три блока, которые дочерние шаблоны могут заполнять. Все, что делает тег block, это сообщает шаблонизатору, что дочерний шаблон может переопределить эти части шаблона, определив свой собственный блок с тем же именем.

Дочерний шаблон может выглядеть следующим образом:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

Ключевым здесь является тег {layout}. Он сообщает шаблонизатору, что этот шаблон “расширяет” другой шаблон. Когда Latte рендерит этот шаблон, сначала он находит родителя – в данном случае layout.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} условие оценено как true или false. Вопреки тому, что вы можете подумать, этот шаблон действительно определяет блок.

{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.latte'}
{var $robots = noindex}

...

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

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

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

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

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

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

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

Советы

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

  • Если вы используете {layout} в шаблоне, он должен быть первым тегом шаблона в этом шаблоне.
  • Тег {layout} имеет псевдоним {extends}.
  • Имя файла расширенного шаблона зависит от загрузчика шаблонов.
  • Вы можете иметь столько блоков, сколько хотите. Помните, что дочерние шаблоны не обязаны определять все родительские блоки, поэтому вы можете заполнить разумные значения по умолчанию в нескольких блоках, а затем определить только те, которые вам нужны позже.

Блоки {block}

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

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

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

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

{* child.Latte *}
{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}                  // prints: foo
bar: {$bar ?? 'not defined'} // prints: not defined

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

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

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

<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'}

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

Вы можете передавать переменные таким образом:

{* since Latte 2.9 *}
{include footer, foo: bar, id: 123}

{* before Latte 2.9 *}
{include footer, foo => bar, id => 123}

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

{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. они могут принимать аргументы
  2. они не могут иметь фильтров
  3. они заключены в теги {define}, и содержимое внутри этих тегов не отправляется на вывод, пока вы их не включите. Благодаря этому вы можете создавать их в любом месте:
{block foo}<p>Hello</p>{/block}
{* prints: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* prints nothing *}

{include bar}
{* prints: <p>World</p> *}

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

{* forms.latte *}
{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, возможное с Latte 2.9.1). Начиная с Latte 2.7, типы параметров также могут быть объявлены: {define input, string $name, ...}.

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

Они включаются так же, как и блок:

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

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

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

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

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

{* child.latte *}
{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}

Советы

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

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

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

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

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

{layout 'layout.latte'}

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

Горизонтальное повторное использование – это способ достижения той же цели, что и множественное наследование, но без сопутствующих сложностей:

{layout 'layout.latte'}

{import 'blocks.latte'}

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

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

{* blocks.latte *}

{block sidebar}...{/block}

В этом примере оператор {import} импортирует блок sidebar в основной шаблон.

Импортируемый шаблон не должен расширять другой шаблон, а его тело должно быть пустым. Однако импортируемый шаблон может импортировать другие шаблоны.

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

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

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

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

{layout 'base.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} {* works, block is defined inside embed *}
		{include hello} {* works, block is local in this template *}
		{include content} {* works, block is defined in embedded template *}
		{include aBlockDefinedInImportedTemplate} {* works *}
		{include outer} {* does not work! - block is in outer layer *}
	{/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} и не ясно, что это – блок или имя файла, добавьте ключевое слово block или file:

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

Примеры использования

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

{include template}

Use Case: Использование 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}