Успадкування та повторне використання шаблонів

Механізми повторного використання та успадкування шаблонів підвищать вашу продуктивність, оскільки кожен шаблон містить лише свій унікальний контент, а повторювані елементи та структури використовуються повторно. Представляємо три концепції: успадкування layout'ів, горизонтальне повторне використання та успадкування одиниць.

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

Успадкування layout'ів {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} оцінена як 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}

Простір поза блоками в дочірньому шаблоні виконується перед рендерингом шаблону layout'у, тому ви можете використовувати його для визначення змінних, таких як {var $foo = bar}, і для поширення даних по всьому ланцюжку успадкування:

{layout 'layout.latte'}
{var $robots = noindex}

...

Багаторівневе успадкування

Ви можете використовувати стільки рівнів успадкування, скільки вам потрібно. Поширеним способом використання успадкування layout'ів є наступний трирівневий підхід:

  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'ів:

  • Якщо ви використовуєте {layout} у шаблоні, це має бути перший тег шаблону в цьому шаблоні.
  • Layout може автоматично знаходитись (як, наприклад, у presenter'ах). У такому випадку, якщо шаблон не повинен мати layout, він повідомляє про це тегом {layout none}.
  • Тег {layout} має псевдонім {extends}.
  • Назва файлу layout'у залежить від завантажувача.
  • Ви можете мати скільки завгодно блоків. Пам'ятайте, що дочірні шаблони не зобов'язані визначати всі батьківські блоки, тому ви можете заповнити розумні значення за замовчуванням у кількох блоках, а потім визначити лише ті, які вам потрібні пізніше.

Блоки {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 ?? 'не визначено'} // виводить: не визначено

Вміст блоку можна змінити за допомогою фільтрів. Наступний приклад видаляє весь 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 намагається робити речі простими, тому, по суті, визначення такі ж, як і блоки, і все, що сказано про блоки, стосується також і визначень. Вони відрізняються від блоків тим, що:

  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.

Хоча успадкування layout'ів шаблонів є однією з найпотужніших функцій 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}

Успадкування одиниць розширює ідею успадкування layout'ів до рівня фрагментів контенту. Тоді як успадкування layout'ів працює з «каркасом документа», який оживляють дочірні шаблони, успадкування одиниць дозволяє створювати каркаси для менших одиниць контенту та повторно використовувати їх де завгодно.

У спадкуванні одиниць ключовим є тег {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} визначають два блоки, які дочірні шаблони можуть заповнити. Так, як у випадку батьківського шаблону при успадкуванні layout'ів. Ви також бачите змінну $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>Це бічна панель</aside>{/block}

single.product.latte

{layout 'product.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Сторінка продукту</main>{/block}

single.service.latte

{layout 'service.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Сторінка сервісу</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}Перша сторінка продукту{/block}
	{block last}Остання сторінка продукту{/block}
{/embed}

service.table.latte

{embed 'pagination.latte', min: 1, max: $services->count}
	{block first}Перша сторінка сервісу{/block}
	{block last}Остання сторінка сервісу{/block}
{/embed}
версія: 3.0