Наследование шаблонов и возможность повторного использования
Механизмы повторного использования и наследования шаблонов повышают вашу производительность, поскольку каждый шаблон содержит только уникальное содержимое, а повторяющиеся элементы и структуры используются повторно. Мы представляем три концепции: наследование макета, горизонтальное повторное использование и наследование единиц.
Концепция наследования шаблонов 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}© 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">
© 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}
...
Многоуровневое наследование
Вы можете использовать столько уровней наследования, сколько необходимо. Одним из распространенных способов использования наследования макетов является следующий трехуровневый подход:
- Создайте шаблон
layout.latte
, в котором будет храниться основной внешний вид вашего сайта. - Создайте шаблон
layout-SECTIONNAME.latte
для каждого раздела вашего сайта. Например,layout-news.latte
,layout-blog.latte
и т.д. Все эти шаблоны расширяютlayout.latte
и включают стили/дизайн для каждого раздела. - Создайте отдельные шаблоны для каждого типа страницы, например, для новостной статьи или записи в блоге. Эти шаблоны расширяют соответствующий шаблон раздела.
Динамическое наследование макета
Вы можете использовать переменную или любое выражение 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}
Если вы отобразите этот шаблон, результат будет точно таким же с тегами блока или без них. Блоки имеют доступ к переменным из внешних диапазонов. Это просто способ сделать его переопределяемым для дочернего шаблона:
{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'}
Выводимые блоки не имеют доступа к переменным активного контекста, за исключением случаев, когда блок определен в том же файле, куда он включен. Однако они имеют доступ к глобальным переменным.
Вы можете передавать переменные в блок следующим образом:
{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 стремится к простоте, поэтому в основном определения – это то же самое, что и блоки, и все, что сказано о блоках, относится и к определениям. Они отличаются от блоков тем, что:
- они заключены в теги
{define}
- они отображаются только при вставке через
{include}
- для них можно задавать параметры, как для функций в PHP
{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-форм.
{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:attributes
для определения таких блоков,
как
<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>
Блоки внутри тегов 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}