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