テンプレートの継承と再利用性
テンプレートの再利用と継承の仕組みは、各テンプレートが固有の内容のみを含み、繰り返される要素や構造は再利用されるため、生産性を向上させることができます。ここでは、レイアウト継承、水平再利用、単位継承の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}© 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がこのテンプレートをレンダリングするとき、まず親を探します。この場合、layout.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">
© 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レベルの方法があります。
- サイトの主要な外観を保持する
layout.latte
テンプレートを作成します。 - サイトの各セクションに対応する
layout-SECTIONNAME.latte
テンプレートを作成します。例えば、layout-news.latte
,layout-blog.latte
などです。これらのテンプレートはすべてlayout.latte
を拡張し、セクション固有のスタイルやデザインを含んでいます。 - ニュース記事やブログ記事など、ページの種類ごとに個別のテンプレートを作成します。これらのテンプレートは、適切なセクションテンプレートを拡張します。
動的なレイアウトの継承
親テンプレートの名前を変数や任意のPHP式で指定することで、動的な継承を行うことができます。
{layout $standalone ? 'minimum.latte' : 'layout.latte'}
また、Latte APIを使用して、レイアウトテンプレートを自動的に選択することもできます。
ヒント
ここでは、レイアウトの継承を行う際のTipsを紹介します。
- テンプレート内で
{layout}
を使用する場合、そのテンプレート内の最初のテンプレートタグでなければなりません。 - レイアウトは自動的に検索することができます(プレゼンターのように)。この場合、テンプレートがレイアウトを持つべきではない場合は、
{layout none}
タグでその旨を表示します。 - タグ
{layout}
はエイリアス{extends}
を持ちます。 - 拡張テンプレートのファイル名は、テンプレート・ローダーによって異なります。
- ブロックはいくつでも用意できます。子テンプレートはすべての親ブロックを定義する必要はないので、いくつかのブロックに合理的なデフォルトを記入し、後で必要なものだけを定義することができることを覚えておいてください。
ブロック{block}
匿名も参照 {block}
ブロックは、テンプレートの特定の部分がどのようにレンダリングされるかを変更する方法を提供しますが、その周りのロジックには何ら干渉しません。次の例で、ブロックがどのように機能するか、そしてより重要なのは、どのように機能しないかを説明します。
{foreach $posts as $post}
{block post}
<h1>{$post->title}</h1>
<p>{$post->body}</p>
{/block}
{/foreach}
このテンプレートをレンダリングした場合、ブロックタグがあってもなくても結果はまったく同じになります。ブロックは外部スコープの変数にアクセスすることができます。これは、子テンプレートでオーバーライド可能にするための手段に過ぎません。
{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:attributeと書くこともできる。
<article n:block=post>
...
</article>
ローカルブロック
すべてのブロックは、同じ名前の親ブロックの内容を上書きします。ただし、ローカルブロックは例外です。これは、クラスのプライベート・メソッドのようなものです。ブロック名の一致により、2つ目のテンプレートで上書きされることを心配することなく、テンプレートを作成することができます。
{block local helper}
...
{/block}
ブロックの印刷{include}
ブロックを特定の場所に印刷するには、{include blockname}
タグを使用します。
<title>{block title}{/block}</title>
<h1>{include title}</h1>
また、別のテンプレートからブロックを表示することもできます。
{include footer from 'main.latte'}
印刷されたブロックは、アクティブなコンテキストの変数にアクセスできません。ただし、ブロックがインクルードされているのと同じファイルに定義されている場合は例外です。ただし、グローバル変数へのアクセスは可能です。
ブロックに変数を渡すには、次のようにします:
{include footer, foo: bar, id: 123}
ブロック名として、変数または PHP
の任意の式を使用することができます。この場合、変数の前に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にはブロックの他に「定義」というものがあります.これは、通常のプログラミング言語における関数に相当するものです。テンプレートの断片を再利用して、同じことを繰り返さないようにするために便利です。
ラテは物事をシンプルにしようとしているので、基本的に定義はブロックと同じであり、ブロックについて言われたことはすべて定義にも当てはまる。定義がブロックと違うのは
- タグで囲まれる
{define}
- 挿入されたときだけレンダリングされる。
{include}
- 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-Peter
,hi-John
,hi-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}
ヒント
ブロックを使った作業のコツを紹介します。
- 最後のトップレベルブロックには終了タグは必要ありません(ブロックはドキュメントの終わりで終了します)。これによって、1つの主ブロックを持つ子テンプレートの記述が簡単になります。
- 読みやすくするために、
{/block}
タグに{/block footer}
のような名前を付けることができます。ただし、この名前はブロック名と一致させる必要があります。大きなテンプレートでは、このテクニックを使うと、どのブロックタグが閉じられているのかがわかりやすくなります。 - 同じテンプレートに同じ名前の複数のブロックタグを直接定義することはできません。しかし、動的なブロック名を使えば、これを実現することができます。
- n:attributesを使って、次のようなブロックを定義することができます。
<h1 n:block=title>Welcome to my awesome homepage</h1>
- ブロックは、出力にフィルタを適用するためだけに名前なしで使用することもできます。
{block|strip} hello {/block}
水平方向の再利用{import}
水平方向の再利用はLatteの3つ目の再利用性・継承の仕組みです。他のテンプレートからブロックを読み込むことができるようになります。ヘルパー関数やtraitでPHPファイルを作成するのと同じようなものです。
テンプレートレイアウトの継承はLatteの最も強力な機能の一つですが、単純な継承に限られています。水平方向の再利用は、複数の継承を実現する方法です。
ブロック定義のセットを用意しよう:
{block sidebar}...{/block}
{block menu}...{/block}
{import}
コマンドを使って、blocks.latte
で定義されたすべてのブロックと定義を別のテンプレートにインポートする:
{import 'blocks.latte'}
{* サイドバーとメニュー・ブロックが使用可能に *}
親テンプレートでブロックをインポートすると(つまり、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タグ内のブロックは、他のブロックとは独立した別レイヤーを形成しています。したがって、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} {* 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}
使用例: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
insingle.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}