テンプレートの継承と再利用

テンプレートの再利用と継承のしくみは、あなたの生産性を高めるためにあります。各テンプレートには固有の内容だけが入り、繰り返し現れる要素や構造は再利用されるからです。ここでは 3 つの考え方を紹介します。Layout InheritanceHorizontal ReuseUnit Inheritance です。

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}&copy; Copyright 2008{/block}
	</div>
</body>
</html>

{block} タグは、子テンプレートが埋められる 3 つのブロックを定義しています。ブロックタグがすることは、子テンプレートが同じ名前のブロックを定義することでこの部分を上書きできる、とテンプレートエンジンに伝えることだけです。

子テンプレートは次のようなものになります。

{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 にある 3 つのブロックタグに気づき、それらを子テンプレートの内容で置き換えます。子テンプレートは footer ブロックを定義していないので、代わりに親テンプレートの内容が使われます。親テンプレートの {block} タグの中身は、常にフォールバックとして使われます。

出力は次のようになります。

<!doctype html>
<html lang="en">
<head>
	<title>My amazing blog</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		<p>Welcome to my awesome homepage.</p>
	</div>
	<div id="footer">
		&copy; Copyright 2008
	</div>
</body>
</html>

子テンプレートでは、ブロックはふつうトップレベルか、別のブロックの中に置きます。たとえば次のようにです。

{block content}
	<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}

また、ブロックは周囲の {if} 条件が真と評価されるか偽と評価されるかに関係なく、常に作られます。ですから、そう見えなくても、次のテンプレートはブロックを定義しています。

{if false}
	{block head}
		<meta name="robots" content="noindex, follow">
	{/block}
{/if}

ブロックの中の出力を条件つきで表示したいなら、代わりに次のようにします。

{block head}
	{if $condition}
		<meta name="robots" content="noindex, follow">
	{/if}
{/block}

子テンプレートのヘッダー部分(つまり最初のブロックや何らかの出力より前)のコードは、レイアウトテンプレートがレンダリングされる前に実行されます。ですから {var $foo = bar} のように変数を定義し、継承の連なり全体にデータを伝えるのに使えます。{layout} を持つテンプレートで、ブロックのあいだやあとに置かれたコードはまったく実行されません。

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

...

現在のテンプレートで変数を作らずにレイアウトにだけ変数を渡したいなら、{layout}(または {extends})タグの中でカンマのあとに直接並べます。

{layout 'layout.latte', robots: noindex}

変数 $robots はレイアウトとそのブロックでは使えますが、現在のテンプレートのブロックでは使えません。明示的に渡された変数は、同じ名前のテンプレートパラメータより優先されます。

多階層の継承

継承の階層は必要なだけ深くできます。レイアウト継承のよくある使い方のひとつが、次の 3 階層の構成です。

  1. サイト全体の見た目と雰囲気を持つ layout.latte テンプレートを作る。
  2. サイトのセクションごとに layout-SECTIONNAME.latte テンプレートを作る。たとえば layout-news.lattelayout-blog.latte などです。これらはすべて layout.latte を継承し、各セクション固有のスタイルとデザインを含みます。
  3. ニュース記事やブログ投稿など、ページの種類ごとに個別のテンプレートを作る。これらは対応するセクションのテンプレートを継承します。

動的なレイアウト継承

親テンプレートの名前には変数や任意の PHP の式を使えるので、継承を動的に振る舞わせられます。

{layout $standalone ? 'minimum.latte' : 'layout.latte'}

Latte の API を使って、レイアウトテンプレートを自動的に選ばせることもできます。

ヒント

レイアウト継承を扱うときのヒントをいくつか挙げます。

  • テンプレートで {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}

さて、子テンプレートをレンダリングすると、ループは parent.latte で定義されたブロックの代わりに、子テンプレート child.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>

ローカルブロック

すべてのブロックは同じ名前の親ブロックの内容を上書きします。ローカルブロックだけは例外です。クラスの private メソッドに相当します。ブロック名がたまたま一致して別のテンプレートに上書きされる心配なく、テンプレートを作れます。

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

ブロックと同じく、定義も現在のコンテキストの変数にはアクセスできず、グローバル変数だけにアクセスできます。例外は、宣言されたパラメータを持たず、静的な名前で参照され、定義されているのと同じファイルの中で挿入される定義です。そうした定義は、挿入元の場所のコンテキスト変数にアクセスできます。

動的なブロック名

ブロック名には任意の PHP の式を使えるので、Latte はブロックの定義に大きな柔軟さを与えます。次の例は hi-Peterhi-Johnhi-Mary という名前の 3 つのブロックを定義します。

{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 における再利用と継承の 3 つめのしくみです。ほかのテンプレートからブロックを読み込めます。PHP でヘルパー関数のファイルを作り、require で読み込むのに似ています。

テンプレートのレイアウト継承は Latte の最も強力な機能のひとつですが、単一継承に限られます。テンプレートが継承できるのはひとつのテンプレートだけです。水平方向の再利用は、多重継承を実現する手段です。

ブロックの定義を持つファイルがあるとします。

{block sidebar}...{/block}

{block menu}...{/block}

{import} コマンドを使って、blocks.latte で定義されたすべてのブロックとDefinitionsを別のテンプレートに取り込みます。

{import 'blocks.latte'}

{* これで sidebar と menu のブロックが使えます *}

親テンプレートでブロックを取り込むと(つまり layout.latte{import} を使うと)、そのブロックはすべての子テンプレートでも使えるようになり、とても実用的です。

取り込まれる側のテンプレート(たとえば blocks.latte)は、ほかのテンプレートを継承してはいけません。つまり {layout} を使えません。ただし、ほかのテンプレートを取り込むことはできます。

{import} タグは {layout} の次、テンプレートの最初のタグであるべきです。テンプレート名には任意の PHP の式を使えます。

{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}

ひとつのテンプレートで {import} はいくつでも使えます。取り込まれた 2 つのテンプレートが同じブロックを定義している場合は、最初のものが勝ちます。ただし主となるテンプレートの優先度が最も高く、取り込まれたどのブロックも上書きできます。

{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}blocks.latte テンプレートの sidebar ブロックを呼びます。

ユニット継承 {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} タグは、子テンプレートが埋められる 2 つのブロックを定義しています。そう、レイアウト継承における親テンプレートの場合とまったく同じです。$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 の外側のブロックから隔離された別の層をつくります。ですから外側のブロックと同じ名前でも衝突せず、影響も受けません。{embed} タグの中で include タグを使うと、ここで作ったブロック、埋め込まれたテンプレートのブロック(ローカルないもの)、そして主テンプレートのブロックのうちローカルであるものを挿入できます。ほかのファイルからブロックを取り込むこともできます。

{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} に式を渡していて、それがブロック名なのかファイル名なのかはっきりしない場合は、キーワード blockfile を付けます。

{embed block $name} ... {/embed}

ユースケース

Latte には継承とコード再利用のさまざまな形があります。分かりやすくするために、主な考え方をまとめておきましょう。

{include template}

ユースケース: layout.latte の中で header.lattefooter.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}

ユースケース: homepage.latteabout.latte の中で layout.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}

ユースケース: single.product.lattesingle.service.lattesidebar.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}

ユースケース: product.table.latteservice.table.lattepagination.latte を埋め込む。

pagination.latte

<div id="pagination">
	<div>{block first}{/block}</div>

	{for $i = $min + 1; $i < $max - 1; $i++}
		<div>{$i}</div>
	{/for}

	<div>{block last}{/block}</div>
</div>

product.table.latte

{embed 'pagination.latte', min: 1, max: $products->count}
	{block first}First Product Page{/block}
	{block last}Last Product Page{/block}
{/embed}

service.table.latte

{embed 'pagination.latte', min: 1, max: $services->count}
	{block first}First Service Page{/block}
	{block last}Last Service Page{/block}
{/embed}
バージョン: 3.x