Наследование и переиспользование шаблонов
*****************************************
.[perex]
Механизмы переиспользования и наследования шаблонов существуют для того, чтобы повысить вашу продуктивность: каждый шаблон содержит только своё уникальное содержимое, а повторяющиеся элементы и структуры используются повторно. Мы представим три концепции: [#Наследование макета], [#Горизонтальное переиспользование] и [#Блочное наследование].
Концепция наследования шаблонов Latte похожа на наследование классов в PHP. Вы определяете **родительский шаблон**, от которого могут наследоваться другие **дочерние шаблоны**, переопределяя части родительского шаблона. Это отлично работает, когда у элементов есть общая структура. Звучит сложно? Не переживайте, это очень просто.
Наследование макета `{layout}` .{toc: Наследование макета}
==========================================================
Рассмотрим наследование макета на примере. Вот родительский шаблон, назовём его `layout.latte`, который задаёт каркас HTML-документа:
```latte
<!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}` задают три блока, которые могут заполнить дочерние шаблоны. Всё, что делает тег блока, - сообщает движку шаблонов, что дочерний шаблон может переопределить эти части, определив собственный блок с тем же именем.
Дочерний шаблон может выглядеть так:
```latte
{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}` в родительском шаблоне всегда служит запасным вариантом.
Вывод может выглядеть так:
```latte
<!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>
```
В дочернем шаблоне блоки обычно размещаются на верхнем уровне или внутри другого блока, например:
```latte
{block content}
<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}
```
Кроме того, блок создаётся всегда, независимо от того, вычисляется ли окружающее условие `{if}` как истинное или ложное. Так что, хотя по виду и не скажешь, этот шаблон блок всё-таки определяет.
```latte
{if false}
{block head}
<meta name="robots" content="noindex, follow">
{/block}
{/if}
```
Если вы хотите, чтобы вывод внутри блока отображался по условию, используйте вместо этого следующее:
```latte
{block head}
{if $condition}
<meta name="robots" content="noindex, follow">
{/if}
{/block}
```
Код в шапке дочернего шаблона (то есть до первого блока или любого вывода) выполняется до отрисовки шаблона макета, поэтому вы можете использовать его для определения переменных вроде `{var $foo = bar}` и передавать данные по всей цепочке наследования. Код, размещённый между блоками или после них в шаблоне с `{layout}`, не выполняется вообще:
```latte
{layout 'layout.latte'}
{var $robots = noindex}
...
```
Если вы хотите передать переменные только в макет, не создавая их в текущем шаблоне, перечислите их прямо в теге `{layout}` (или `{extends}`) после запятой: .{data-version:3.1.5}
```latte
{layout 'layout.latte', robots: noindex}
```
Переменная `$robots` будет доступна в макете и его блоках, но не в блоках текущего шаблона. Явно переданная переменная также имеет приоритет над одноимённым параметром шаблона.
Многоуровневое наследование
---------------------------
Вы можете использовать сколько угодно уровней наследования. Один из распространённых способов применения наследования макетов - следующий трёхуровневый подход:
1) Создайте шаблон `layout.latte`, который держит общий облик вашего сайта.
2) Создайте шаблон `layout-SECTIONNAME.latte` для каждого раздела сайта. Например, `layout-news.latte`, `layout-blog.latte` и так далее. Все эти шаблоны расширяют `layout.latte` и содержат стили и оформление, специфичные для каждого раздела.
3) Создайте отдельные шаблоны для каждого типа страницы, например для новостной заметки или записи блога. Эти шаблоны расширяют соответствующий шаблон раздела.
Динамическое наследование макета
--------------------------------
В качестве имени родительского шаблона можно использовать переменную или любое выражение PHP, так что наследование может вести себя динамически:
```latte
{layout $standalone ? 'minimum.latte' : 'layout.latte'}
```
Вы можете также использовать API Latte, чтобы выбирать шаблон макета [автоматически |develop#Автоматический поиск макета].
Советы
------
Вот несколько советов по работе с наследованием макетов:
- Если вы используете `{layout}` в шаблоне, он должен стоять в шапке шаблона, то есть до любого вывода. Перед ним могут стоять только теги, не производящие вывода (такие как `{var}`, `{templateType}`, `{import}` или комментарии).
- Макет может [находиться автоматически |develop#Автоматический поиск макета] (как в [презентерах |application:templates#Поиск шаблона]). В этом случае, если у шаблона не должно быть макета, он указывает это тегом `{layout none}`. И наоборот, `{layout auto}` (или `{extends auto}`) возвращает автоматический поиск макета.
- У тега `{layout}` есть псевдоним `{extends}`.
- Имя файла макета зависит от [загрузчика |loaders].
- Блоков может быть сколько угодно. Помните, что дочерние шаблоны не обязаны определять все родительские блоки, поэтому вы можете заполнить разумными значениями по умолчанию несколько блоков, а позже определять только те, которые нужны.
Блоки `{block}` .{toc: Блоки}
=============================
.[note]
См. также анонимный [`{block}` |tags#{block}]
Блок даёт способ изменить то, как отрисовывается определённая часть шаблона, но никак не вмешивается в логику вокруг неё. Проиллюстрируем, как работает блок и, что важнее, как он не работает, следующим примером:
```latte .{file: parent.latte}
{foreach $posts as $post}
{block post}
<h1>{$post->title}</h1>
<p>{$post->body}</p>
{/block}
{/foreach}
```
Если вы отрисуете этот шаблон, результат будет ровно таким же и с тегами `{block}`, и без них. Блоки имеют доступ к переменным из внешних областей видимости. Они лишь дают возможность переопределить себя в дочернем шаблоне:
```latte .{file: child.latte}
{layout 'parent.latte'}
{block post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/block}
```
Теперь при отрисовке дочернего шаблона цикл будет использовать блок, определённый в дочернем шаблоне `child.latte`, вместо того, что определён в `parent.latte`; выполняемый шаблон тогда равнозначен следующему:
```latte
{foreach $posts as $post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/foreach}
```
Однако если мы создадим внутри именованного блока новую переменную или заменим значение существующей, изменение будет видно только внутри блока:
```latte
{var $foo = 'foo'}
{block post}
{do $foo = 'new value'}
{var $bar = 'bar'}
{/block}
foo: {$foo} // выводит: foo
bar: {$bar ?? 'not defined'} // выводит: not defined
```
Содержимое блока можно изменить [фильтрами |syntax#Фильтры]. Следующий пример убирает весь HTML и переводит текст в верхний регистр:
```latte
<title>{block title|stripHtml|capitalize}...{/block}</title>
```
Тег можно записать и как [n:атрибут |syntax#n:атрибуты]:
```latte
<article n:block=post>
...
</article>
```
Локальные блоки
---------------
Каждый блок переопределяет содержимое одноимённого родительского блока, за исключением локальных блоков. Они подобны приватным методам классов. Вы можете создать шаблон, не опасаясь, что из-за случайного совпадения имён блоков их перезапишет другой шаблон.
```latte
{block local helper}
...
{/block}
```
Вывод блоков `{include}` .{toc: Вывод блоков}
---------------------------------------------
.[note]
См. также [`{include file}` |tags#include]
Чтобы вывести блок в определённом месте, используйте тег `{include blockname}`:
```latte
<title>{block title}{/block}</title>
<h1>{include title}</h1>
```
Вы можете вывести и блок из другого шаблона:
```latte
{include footer from 'main.latte'}
```
Отрисовываемый блок не имеет доступа к переменным активного контекста, если только блок не определён в том же файле, где он подключается. Зато он имеет доступ к глобальным переменным.
Передать переменные в блок можно так:
```latte
{include footer, foo: bar, id: 123}
```
Имя блока может быть переменной или любым выражением PHP. В этом случае добавьте перед переменной ключевое слово `block`, чтобы Latte во время компиляции знал, что это блок, а не [подключаемый шаблон |tags#include], имя которого тоже могло бы быть в переменной:
```latte
{var $name = footer}
{include block $name}
```
Блок можно отрисовать и внутри самого себя, что полезно, например, при отрисовке древовидной структуры:
```latte
{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` означает текущий блок.
Отрисованное содержимое блока можно изменить [фильтрами |syntax#Фильтры]. Следующий пример убирает весь HTML и переводит текст в верхний регистр:
```latte
<title>{include heading|stripHtml|capitalize}</title>
```
Родительский блок
-----------------
Если вам нужно вывести содержимое блока из родительского шаблона, используйте `{include parent}`. Это полезно, когда вы хотите дополнить содержимое родительского блока, а не полностью его переопределить.
```latte
{block footer}
{include parent}
<a href="https://github.com/nette">GitHub</a>
<a href="https://twitter.com/nettefw">Twitter</a>
{/block}
```
Определения `{define}` .{toc: Определения}
------------------------------------------
Помимо блоков в Latte есть и "определения". В обычных языках программирования их можно сравнить с функциями. Они полезны для переиспользования фрагментов шаблона, чтобы избежать повторов.
Latte старается держать всё простым, поэтому по сути определения - то же самое, что блоки, и **всё сказанное о блоках относится и к определениям**. От блоков они отличаются тем, что:
1) заключены в теги `{define}`
2) отрисовываются только при вставке через `{include}`
3) для них можно определить параметры, как для функций в PHP
```latte
{block foo}<p>Hello</p>{/block}
{* выводит: <p>Hello</p> *}
{define bar}<p>World</p>{/define}
{* не выводит ничего *}
{include bar}
{* выводит: <p>World</p> *}
```
Представьте, что у вас есть вспомогательный шаблон с набором определений того, как рисовать HTML-формы.
```latte .{file: 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`). Можно объявить и типы параметров: `{define input, string $name, ...}`.
Шаблон с определениями загружается через [`{import}` |#Горизонтальное переиспользование]. Сами определения отрисовываются [так же, как блоки |#Вывод блоков]:
```latte
<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>
```
Как и блоки, определения не имеют доступа к переменным активного контекста, только к глобальным переменным. Исключение составляет определение без объявленных параметров, к которому обращаются по статическому имени и которое подключается в том же файле, где определено: такое определение имеет доступ к переменным контекста того места, откуда оно подключается.
Динамические имена блоков
-------------------------
Latte даёт большую гибкость в определении блоков, потому что имя блока может быть любым выражением PHP. Этот пример определяет три блока с именами `hi-Peter`, `hi-John` и `hi-Mary`:
```latte .{file: parent.latte}
{foreach [Peter, John, Mary] as $name}
{block "hi-$name"}Hi, I am {$name}.{/block}
{/foreach}
```
В дочернем шаблоне мы затем можем переопределить, например, только один блок:
```latte .{file: child.latte}
{block hi-John}Hello. I am {$name}.{/block}
```
Так что вывод будет выглядеть так:
```latte
Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.
```
Проверка существования блока `{ifset}` .{toc: Проверка существования блока}
---------------------------------------------------------------------------
.[note]
См. также [`{ifset $var}` |tags#{ifset} {elseifset}]
Используйте проверку `{ifset blockname}`, чтобы узнать, существует ли блок (или несколько блоков) в текущем контексте:
```latte
{ifset footer}
...
{/ifset}
{ifset footer, header, main}
...
{/ifset}
```
Имя блока может быть переменной или любым выражением PHP. В этом случае добавьте перед переменной ключевое слово `block`, чтобы уточнить, что это не проверка существования [переменных |tags#{ifset} {elseifset}]:
```latte
{ifset block $name}
...
{/ifset}
```
Существование блоков проверяет и функция [`hasBlock()` |functions#hasBlock()]:
```latte
{if hasBlock(header) || hasBlock(footer)}
...
{/if}
```
Советы
------
Несколько советов по работе с блоками:
- Последнему блоку верхнего уровня закрывающий тег не нужен (блок заканчивается вместе с концом документа). Это упрощает написание дочерних шаблонов, содержащих один основной блок.
- Ради читаемости вы можете при желании указать имя блока и в теге `{/block}`, например `{/block footer}`. Но имя должно совпадать с именем блока. В крупных шаблонах этот приём помогает видеть, какие теги блоков закрываются.
- Нельзя напрямую определить в одном шаблоне несколько тегов блоков с одинаковым именем. Но этого можно добиться с помощью [#Динамические имена блоков].
- Для определения блоков можно использовать [n:атрибуты |syntax#n:атрибуты], например `<h1 n:block=title>Welcome to my awesome homepage</h1>`
- Блоки можно использовать и без имён, только чтобы применить к выводу [фильтры |syntax#Фильтры]: `{block|strip} hello {/block}`
Горизонтальное переиспользование `{import}` .{toc: Горизонтальное переиспользование}
====================================================================================
Горизонтальное переиспользование - третий механизм переиспользования и наследования в Latte. Оно позволяет загружать блоки из других шаблонов. Это похоже на создание файла со вспомогательными функциями в PHP и его последующую загрузку через `require`.
Наследование макетов - одна из самых мощных возможностей Latte, но оно ограничено простым наследованием: шаблон может расширять только один другой шаблон. Горизонтальное переиспользование - способ добиться множественного наследования.
Пусть у нас есть файл с определениями блоков:
```latte .{file: blocks.latte}
{block sidebar}...{/block}
{block menu}...{/block}
```
С помощью команды `{import}` мы импортируем все блоки и [#Определения], определённые в `blocks.latte`, в другой шаблон:
```latte .{file: child.latte}
{import 'blocks.latte'}
{* теперь можно использовать блоки sidebar и menu *}
```
Если вы импортируете блоки в родительском шаблоне (то есть используете `{import}` в `layout.latte`), блоки будут доступны и во всех дочерних шаблонах, что очень удобно.
Шаблон, предназначенный для импорта (например, `blocks.latte`), не должен [расширять |#Наследование макета] другой шаблон, то есть использовать `{layout}`. Зато он может импортировать другие шаблоны.
Тег `{import}` должен быть первым тегом шаблона после `{layout}`. Имя шаблона может быть любым выражением PHP:
```latte
{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}
```
Вы можете использовать в шаблоне сколько угодно инструкций `{import}`. Если два импортированных шаблона определяют один и тот же блок, побеждает первый. Однако у главного шаблона наивысший приоритет, и он может переопределить любой импортированный блок.
Тег `{import}` может также передать аргументы в импортируемый шаблон, например `{import 'blocks.latte', foo: 1}`. Эти аргументы затем доступны как переменные в импортированных блоках и определениях.
Содержимое перезаписанных блоков можно сохранить, вставив блок так же, как [#Родительский блок]:
```latte
{layout 'layout.latte'}
{import 'blocks.latte'}
{block sidebar}
{include parent}
{/block}
{block title}...{/block}
{block content}...{/block}
```
В этом примере `{include parent}` вызывает блок `sidebar` из шаблона `blocks.latte`.
Блочное наследование `{embed}` .{toc: Блочное наследование}
===========================================================
Блочное наследование распространяет идею наследования макетов на уровень фрагментов содержимого. Наследование макетов работает с "каркасами документов", которые оживляют дочерние шаблоны, а блочное наследование позволяет создавать каркасы для меньших единиц содержимого и переиспользовать их где угодно.
В блочном наследовании ключевую роль играет тег `{embed}`. Он объединяет поведение `{include}` и `{layout}`. Он позволяет встроить содержимое другого шаблона или блока и при желании передать переменные, как `{include}`. Он также позволяет переопределить любой блок, определённый внутри встраиваемого шаблона, как `{layout}`.
Для примера возьмём элемент "аккордеон". Взгляните на каркас элемента, хранящийся в шаблоне `collapsible.latte`:
```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:
```latte
{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}
```
Вывод может выглядеть так:
```latte
<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}` вы можете вставить блоки, созданные здесь, блоки встраиваемого шаблона (которые *не являются* [локальными |#Локальные блоки]), а также блоки главного шаблона, которые локальными *являются*. Вы можете также [импортировать блоки |#Горизонтальное переиспользование] из других файлов:
```latte
{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}` можно встраивать не только шаблоны, но и другие блоки, поэтому предыдущий пример можно было записать так:
```latte
{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`:
```latte
{embed block $name} ... {/embed}
```
Сценарии использования
======================
В Latte есть разные виды наследования и переиспользования кода. Для наглядности обобщим главные концепции:
`{include template}`
--------------------
**Сценарий**: использование `header.latte` и `footer.latte` внутри `layout.latte`.
`header.latte`
```latte
<nav>
<div>Home</div>
<div>About</div>
</nav>
```
`footer.latte`
```latte
<footer>
<div>Copyright</div>
</footer>
```
`layout.latte`
```latte
{include 'header.latte'}
<main>{block main}{/block}</main>
{include 'footer.latte'}
```
`{layout}`
----------
**Сценарий**: расширение `layout.latte` в `homepage.latte` и `about.latte`.
`layout.latte`
```latte
{include 'header.latte'}
<main>{block main}{/block}</main>
{include 'footer.latte'}
```
`homepage.latte`
```latte
{layout 'layout.latte'}
{block main}
<p>Homepage</p>
{/block}
```
`about.latte`
```latte
{layout 'layout.latte'}
{block main}
<p>About page</p>
{/block}
```
`{import}`
----------
**Сценарий**: использование `sidebar.latte` в `single.product.latte` и `single.service.latte`.
`sidebar.latte`
```latte
{block sidebar}<aside>This is sidebar</aside>{/block}
```
`single.product.latte`
```latte
{layout 'product.layout.latte'}
{import 'sidebar.latte'}
{block main}<main>Product page</main>{/block}
```
`single.service.latte`
```latte
{layout 'service.layout.latte'}
{import 'sidebar.latte'}
{block main}<main>Service page</main>{/block}
```
`{define}`
----------
**Сценарий**: функции, которые принимают переменные и что-то отрисовывают.
`form.latte`
```latte
{define form-input, $name, $value, $type = 'text'}
<input type={$type} name={$name} value={$value}>
{/define}
```
`profile.service.latte`
```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`
```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`
```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`
```latte
{embed 'pagination.latte', min: 1, max: $services->count}
{block first}First Service Page{/block}
{block last}Last Service Page{/block}
{/embed}
```
Наследование и переиспользование
шаблонов
Механизмы переиспользования и наследования шаблонов
существуют для того, чтобы повысить вашу продуктивность: каждый шаблон
содержит только своё уникальное содержимое, а повторяющиеся элементы
и структуры используются повторно. Мы представим три концепции: Наследование макета, Горизонтальное переиспользование и Блочное наследование.
Концепция наследования шаблонов 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}