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

テンプレートの再利用と継承のメカニズムは、各テンプレートが独自のコンテンツのみを含み、繰り返される要素と構造が再利用されるため、生産性を向上させます。3つのコンセプトを紹介します:レイアウト継承水平再利用、およびユニット継承

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つのブロックを定義します。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 内の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} 条件がtrueまたはfalseに評価されるかに関わらず、ブロックは常に作成されます。したがって、そうは見えなくても、このテンプレートはブロックを定義します。

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

ブロック内の出力を条件付きで表示したい場合は、代わりに次を使用してください:

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

子テンプレートのブロック外のスペースは、レイアウトテンプレートがレンダリングされる前に実行されるため、{var $foo = bar} のような変数を定義し、継承チェーン全体にデータを伝播するために使用できます:

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

...

多階層継承

必要なだけ多くの継承レベルを使用できます。レイアウト継承を使用する一般的な方法は、次の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} を使用する場合、それはそのテンプレートの最初のテンプレートタグでなければなりません。
  • レイアウトは自動的に検索されることがあります(例えばPresenterのように)。その場合、テンプレートがレイアウトを持つべきでない場合は、{layout none} タグで示します。
  • {layout} タグにはエイリアス {extends} があります。
  • レイアウトファイル名はloaderに依存します。
  • 好きなだけブロックを持つことができます。子テンプレートはすべての親ブロックを定義する必要はないことを覚えておいてください。そのため、いくつかのブロックに適切なデフォルト値を入力し、後で必要なものだけを定義することができます。

ブロック {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}                  // prints: foo
bar: {$bar ?? 'not defined'} // prints: 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式を使用できます。その場合、コンパイル時にLatteがそれがブロックであり、テンプレートの挿入(その名前も変数にある可能性がある)ではないことを知るために、変数の前にキーワード block を追加します:

{var $name = footer}
{include block $name}

ブロックはそれ自体の中でレンダリングすることもでき、これは例えばツリー構造をレンダリングするのに役立ちます:

{define menu, $items}
<ul>
	{foreach $items as $item}
		<li>
		{if is_array($item)}
			{include menu, $item}
		{else}
			{$item}
		{/if}
		</li>
	{/foreach}
</ul>
{/define}

{include menu, ...} の代わりに {include this, ...} と書くことができます。ここで this は現在のブロックを意味します。

レンダリングされるブロックはフィルタを使用して変更できます。次の例では、すべてのHTMLを削除し、大文字小文字を変更します:

<title>{include heading|stripHtml|capitalize}</title>

親ブロック

親テンプレートからブロックのコンテンツを出力する必要がある場合は、{include parent} を使用します。これは、親ブロックのコンテンツを完全に上書きするのではなく、補完したい場合に便利です。

{block footer}
	{include parent}
	<a href="https://github.com/nette">GitHub</a>
	<a href="https://twitter.com/nettefw">Twitter</a>
{/block}

定義 {define}

ブロックに加えて、Latteには「定義」もあります。通常のプログラミング言語では、これらを関数と比較します。テンプレートのフラグメントを再利用して繰り返しを避けるのに役立ちます。

Latteは物事をシンプルにしようと努めているため、基本的に定義はブロックと同じであり、ブロックについて言われていることはすべて定義にも当てはまります。ブロックとの違いは次の点です:

  1. {define} タグで囲まれている
  2. {include} を介して挿入された場合にのみレンダリングされる
  3. PHPの関数と同様にパラメータを定義できる
{block foo}<p>Hello</p>{/block}
{* prints: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* prints nothing *}

{include bar}
{* prints: <p>World</p> *}

HTMLフォームを描画する方法の定義のコレクションを含むヘルパーテンプレートがあると想像してください。

{define input, $name, $value, $type = 'text'}
	<input type={$type} name={$name} value={$value}>
{/define}

{define textarea, $name, $value}
	<textarea name={$name}>{$value}</textarea>
{/define}

引数は常にオプションであり、デフォルト値が指定されていない場合はデフォルト値 null になります(ここでは 'text'$type のデフォルト値です)。パラメータの型も宣言できます:{define input, string $name, ...}

定義を含むテンプレートは {import} を使用してロードします。定義自体はブロックと同じ方法でレンダリングされます

<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>

定義はアクティブなコンテキストの変数にアクセスできませんが、グローバル変数にはアクセスできます。

動的ブロック名

Latteはブロックの定義において大きな柔軟性を提供します。なぜなら、ブロック名は任意のPHP式にすることができるからです。この例では、hi-Peterhi-Johnhi-Mary という名前の3つのブロックを定義します:

{foreach [Peter, John, Mary] as $name}
	{block "hi-$name"}Hi, I am {$name}.{/block}
{/foreach}

子テンプレートでは、例えば1つのブロックだけを再定義できます:

{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}。ただし、名前はブロック名と一致する必要があります。大きなテンプレートでは、このテクニックはどのブロックタグが閉じられているかを判断するのに役立ちます。
  • 同じテンプレート内で同じ名前の複数のブロックタグを直接定義することはできません。ただし、これは動的ブロック名を使用して実現できます。
  • <h1 n:block=title>Welcome to my awesome homepage</h1> のようにブロックを定義するためにn:属性を使用できます。
  • ブロックは、フィルタを適用するためだけに名前なしで使用することもできます:{block|strip} hello {/block}

水平再利用 {import}

水平再利用は、Latteにおける再利用と継承のための3番目のメカニズムです。これにより、他のテンプレートからブロックをロードできます。これは、PHPでヘルパー関数のファイルを作成し、それを require を使用してロードするのと似ています。

テンプレートのレイアウト継承はLatteの最も強力な機能の1つですが、単純な継承に限定されています – テンプレートは他の1つのテンプレートしか拡張できません。水平再利用は、多重継承を実現する方法です。

ブロック定義を含むファイルがあるとします:

{block sidebar}...{/block}

{block menu}...{/block}

{import} コマンドを使用して、blocks.latte で定義されたすべてのブロックと定義を別のテンプレートにインポートします:

{import 'blocks.latte'}

{* これで sidebar と menu ブロックを使用できます *}

親テンプレートでブロックをインポートする場合(つまり、layout.latte{import} を使用する場合)、ブロックはすべての子テンプレートでも利用可能になり、これは非常に実用的です。

インポートされることを意図したテンプレート(例:blocks.latte)は、別のテンプレートを拡張してはなりません。つまり、{layout} を使用してはなりません。ただし、他のテンプレートをインポートすることはできます。

{import} タグは、{layout} の後のテンプレートの最初のタグである必要があります。テンプレート名は任意のPHP式にすることができます:

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

テンプレート内で好きなだけ {import} コマンドを使用できます。2つのインポートされたテンプレートが同じブロックを定義する場合、最初のものが優先されます。ただし、メインテンプレートが最も高い優先度を持ち、インポートされたブロックを上書きできます。

上書きされたブロックのコンテンツは、親ブロックを挿入するのと同じ方法でブロックを挿入することによって保持できます:

{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} タグ内で include タグを使用すると、ここで作成されたブロック、埋め込みテンプレートからのブロック([ローカル |#lokální bloky]ではないもの)、および逆にローカルであるメインテンプレートからのブロックを挿入できます。他のファイルからブロックをインポートすることもできます:

{block outer}…{/block}
{block local hello}…{/block}

{embed 'collapsible.latte', modifierClass: my-style}
	{import 'blocks.latte'}

	{block inner}…{/block}

	{block title}
		{include inner} {* works, block is defined inside embed *}
		{include hello} {* works, block is local in this template *}
		{include content} {* works, block is defined in embedded template *}
		{include aBlockDefinedInImportedTemplate} {* works *}
		{include outer} {* does not work! - block is in outer layer *}
	{/block}
{/embed}

埋め込みテンプレートはアクティブなコンテキストの変数にアクセスできませんが、グローバル変数にはアクセスできます。

{embed} を使用すると、テンプレートだけでなく他のブロックも埋め込むことができるため、前の例はこのように書くことができます:

{define collapsible}
<section class="collapsible {$modifierClass}">
	<h4 class="collapsible__title">
		{block title}{/block}
	</h4>
	...
</section>
{/define}


{embed collapsible, modifierClass: my-style}
	{block title}
		Hello World
	{/block}
	...
{/embed}

{embed} に式を渡し、それがブロック名なのかファイル名なのかが不明な場合は、キーワード block または file を追加します:

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

ユースケース

Latteには、さまざまな種類の継承とコードの再利用があります。より明確にするために、主要なコンセプトを要約しましょう:

{include template}

ユースケース: 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.latte 内の sidebar.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.0