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

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

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

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

Рассмотрим наследование шаблона макета, то есть 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, что этот шаблон «расширяет» другой шаблон. Когда Latte отрисовывает этот шаблон, он сначала находит родительский шаблон — в данном случае layout.latte.

В этот момент Latte замечает три тега block в 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.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 none}.
  • Тег {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:attribut:

<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}, сколько хотите. Если два импортированных шаблона определяют один и тот же блок, выигрывает первый из них. Однако наивысший приоритет имеет основной шаблон, который может переопределить любой импортированный блок.

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

{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>

Блоки внутри вставленных тегов образуют отдельный слой, независимый от других блоков. Поэтому они могут иметь то же имя, что и блок вне вставки, и никак не затрагиваются. С помощью тега 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} выражение и неясно, является ли это именем блока или файла, мы добавляем ключевое слово 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.0