Héritage et réutilisabilité des templates

Les mécanismes de réutilisation et d'héritage des templates sont là pour doper votre productivité, car chaque template ne contient que son contenu propre, et les éléments et structures répétés sont réutilisés. Nous présentons trois concepts : Héritage de layout, Réutilisation horizontale et Héritage unitaire.

Le concept d'héritage des templates de Latte est proche de l'héritage de classes en PHP. Vous définissez un template parent dont d'autres templates enfants peuvent hériter et dont ils peuvent redéfinir des parties. Cela fonctionne à merveille quand des éléments partagent une structure commune. Cela vous semble compliqué ? Rassurez-vous, c'est très simple.

Héritage de layout {layout}

Examinons l'héritage de template de layout sur un exemple. Voici un template parent, appelons-le layout.latte, qui définit le squelette d'un document 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>

Les balises {block} définissent trois blocs que les templates enfants peuvent remplir. Tout ce que fait la balise block, c'est indiquer au moteur de templates qu'un template enfant peut redéfinir ces parties en définissant son propre bloc du même nom.

Un template enfant pourrait ressembler à ceci :

{layout 'layout.latte'}

{block title}My amazing blog{/block}

{block content}
	<p>Welcome to my awesome homepage.</p>
{/block}

La balise {layout} est ici la clé. Elle indique à Latte que ce template “étend” un autre template. Quand Latte rend ce template, il localise d'abord le template parent, ici layout.latte.

À ce moment, Latte repère les trois balises block dans layout.latte et remplace ces blocs par le contenu du template enfant. Comme le template enfant n'a pas défini le bloc footer, c'est le contenu du template parent qui est utilisé. Le contenu d'une balise {block} du template parent sert toujours de valeur de repli.

La sortie pourrait ressembler à ceci :

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

Dans un template enfant, les blocs se placent généralement au niveau supérieur ou à l'intérieur d'un autre bloc, par exemple :

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

De plus, un bloc sera toujours créé, que la condition {if} environnante soit évaluée à vrai ou à faux. Ainsi, même si cela n'en a pas l'air, ce template définit bien le bloc.

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

Si vous voulez que la sortie du bloc s'affiche sous condition, écrivez plutôt ceci :

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

Le code placé dans l'en-tête du template enfant (c'est-à-dire avant le premier bloc ou toute sortie) est exécuté avant le rendu du template de layout ; vous pouvez donc y définir des variables comme {var $foo = bar} et propager des données dans toute la chaîne d'héritage. Le code placé entre les blocs ou après eux dans un template avec {layout} n'est pas exécuté du tout :

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

...

Si vous voulez passer des variables uniquement au layout, sans les créer dans le template courant, listez-les directement dans la balise {layout} (ou {extends}) après une virgule :

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

La variable $robots sera disponible dans le layout et ses blocs, mais pas dans les blocs du template courant. Une variable passée explicitement l'emporte aussi sur un paramètre de template du même nom.

Héritage à plusieurs niveaux

Vous pouvez utiliser autant de niveaux d'héritage que nécessaire. Une façon courante d'exploiter l'héritage de layout est l'approche à trois niveaux suivante :

  1. Créez un template layout.latte qui porte l'apparence générale de votre site.
  2. Créez un template layout-SECTIONNAME.latte pour chaque section du site. Par exemple layout-news.latte, layout-blog.latte, etc. Tous ces templates étendent layout.latte et intègrent les styles et le design propres à chaque section.
  3. Créez des templates individuels pour chaque type de page, comme un article d'actualité ou un billet de blog. Ces templates étendent le template de section approprié.

Héritage dynamique

Vous pouvez utiliser une variable ou n'importe quelle expression PHP comme nom du template parent : l'héritage peut donc se comporter de façon dynamique :

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

Vous pouvez aussi utiliser l'API de Latte pour choisir le template de layout automatiquement.

Conseils

Voici quelques conseils pour travailler avec l'héritage de layout :

  • Si vous utilisez {layout} dans un template, il doit se trouver dans l'en-tête du template, c'est-à-dire avant toute sortie. Seules des balises qui ne produisent aucune sortie (comme {var}, {templateType}, {import} ou les commentaires) peuvent le précéder.
  • Le layout peut être trouvé automatiquement (comme dans les presenters). Dans ce cas, si le template ne doit pas avoir de layout, il l'indique avec la balise {layout none}. À l'inverse, {layout auto} (ou {extends auto}) rétablit la recherche automatique du layout.
  • La balise {layout} a un alias : {extends}.
  • Le nom du fichier de layout dépend du loader.
  • Vous pouvez avoir autant de blocs que vous voulez. Souvenez-vous que les templates enfants n'ont pas à définir tous les blocs du parent : vous pouvez donc remplir plusieurs blocs avec des valeurs par défaut raisonnables, puis ne définir que ceux dont vous avez besoin.

Blocs {block}

Voyez aussi le {block} anonyme

Un bloc permet de changer la façon dont une certaine partie d'un template est rendue, sans interférer en quoi que ce soit avec la logique qui l'entoure. Illustrons comment un bloc fonctionne et, surtout, comment il ne fonctionne pas, avec l'exemple suivant :

{foreach $posts as $post}
{block post}
	<h1>{$post->title}</h1>
	<p>{$post->body}</p>
{/block}
{/foreach}

Si vous rendez ce template, le résultat sera exactement le même avec ou sans les balises {block}. Les blocs ont accès aux variables des portées extérieures. Ils offrent simplement la possibilité d'être redéfinis par un template enfant :

{layout 'parent.latte'}

{block post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/block}

Désormais, lors du rendu du template enfant, la boucle utilisera le bloc défini dans le template enfant child.latte au lieu de celui défini dans parent.latte ; le template exécuté équivaut alors à ceci :

{foreach $posts as $post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/foreach}

En revanche, si nous créons une nouvelle variable à l'intérieur d'un bloc nommé ou remplaçons la valeur d'une variable existante, le changement ne sera visible qu'à l'intérieur du bloc :

{var $foo = 'foo'}
{block post}
	{do $foo = 'new value'}
	{var $bar = 'bar'}
{/block}

foo: {$foo}                  // affiche : foo
bar: {$bar ?? 'not defined'} // affiche : not defined

Le contenu d'un bloc peut être modifié par des filtres. L'exemple suivant supprime tout le HTML et met le texte en majuscules :

<title>{block title|stripHtml|capitalize}...{/block}</title>

La balise peut aussi s'écrire comme un n:attribut :

<article n:block=post>
	...
</article>

Blocs locaux

Chaque bloc redéfinit le contenu du bloc parent du même nom, sauf les blocs locaux. Ils sont l'équivalent des méthodes privées d'une classe. Vous pouvez créer un template sans craindre qu'une coïncidence de noms de blocs ne conduise un autre template à les écraser.

{block local helper}
	...
{/block}

Rendu des blocs {include}

Voyez aussi {include file}

Pour rendre un bloc à un endroit précis, utilisez la balise {include blockname} :

<title>{block title}{/block}</title>

<h1>{include title}</h1>

Vous pouvez aussi rendre un bloc issu d'un autre template :

{include footer from 'main.latte'}

Le bloc rendu n'a pas accès aux variables du contexte actif, sauf s'il est défini dans le même fichier que celui où il est inclus. Il a en revanche accès aux variables globales.

Vous pouvez passer des variables au bloc de cette façon :

{include footer, foo: bar, id: 123}

Le nom du bloc peut être une variable ou n'importe quelle expression PHP. Dans ce cas, ajoutez le mot-clé block avant la variable, afin que Latte sache à la compilation qu'il s'agit d'un bloc et non d'un template inclus, dont le nom pourrait lui aussi se trouver dans une variable :

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

Un bloc peut aussi être rendu à l'intérieur de lui-même, ce qui est utile par exemple pour rendre une structure arborescente :

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

Au lieu de {include menu, ...}, nous pouvons aussi écrire {include this, ...}, où this désigne le bloc courant.

Le contenu rendu d'un bloc peut être modifié par des filtres. L'exemple suivant supprime tout le HTML et met le texte en majuscules :

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

Bloc parent

Si vous devez afficher le contenu du bloc du template parent, utilisez {include parent}. C'est utile quand vous voulez compléter le contenu du bloc parent plutôt que de le remplacer entièrement.

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

Définitions {define}

Outre les blocs, Latte connaît aussi les “définitions”. Dans les langages de programmation courants, on les comparerait à des fonctions. Elles sont utiles pour réutiliser des fragments de template et éviter les répétitions.

Latte s'efforce de rester simple, si bien que les définitions sont pour l'essentiel identiques aux blocs, et tout ce qui a été dit des blocs vaut aussi pour les définitions. Elles s'en distinguent en ceci :

  1. elles sont encadrées par des balises {define}
  2. elles ne sont rendues que lorsqu'on les insère via {include}
  3. vous pouvez leur définir des paramètres, comme pour les fonctions en PHP
{block foo}<p>Hello</p>{/block}
{* affiche : <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* n'affiche rien *}

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

Imaginez que vous ayez un template auxiliaire avec une collection de définitions décrivant comment dessiner des formulaires 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}

Les arguments sont toujours facultatifs, avec null pour valeur par défaut, sauf si une valeur par défaut est précisée (ici 'text' pour $type). Les types des paramètres peuvent aussi être déclarés : {define input, string $name, ...}.

Le template contenant les définitions se charge avec {import}. Les définitions elles-mêmes se rendent de la même façon que les blocs :

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

Comme les blocs, les définitions n'ont pas accès aux variables du contexte actif, seulement aux variables globales. Font exception les définitions sans paramètres déclarés, référencées par un nom statique et incluses dans le fichier même où elles sont définies : celles-là ont bien accès aux variables du contexte d'où elles sont incluses.

Noms de blocs dynamiques

Latte offre une grande souplesse dans la définition des blocs, car le nom d'un bloc peut être n'importe quelle expression PHP. Cet exemple définit trois blocs nommés hi-Peter, hi-John et hi-Mary :

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

Dans le template enfant, nous pouvons ensuite redéfinir un seul bloc, par exemple :

{block hi-John}Hello. I am {$name}.{/block}

La sortie ressemblera donc à ceci :

Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.

Vérification de l'existence des blocs {ifset}

Voyez aussi {ifset $var}

Utilisez le test {ifset blockname} pour vérifier si un bloc (ou plusieurs blocs) existe dans le contexte courant :

{ifset footer}
	...
{/ifset}

{ifset footer, header, main}
	...
{/ifset}

Le nom du bloc peut être une variable ou n'importe quelle expression PHP. Dans ce cas, ajoutez le mot-clé block avant la variable pour préciser qu'il ne s'agit pas d'un test d'existence de variables :

{ifset block $name}
	...
{/ifset}

L'existence des blocs est également vérifiée par la fonction hasBlock() :

{if hasBlock(header) || hasBlock(footer)}
	...
{/if}

Conseils

Quelques conseils pour travailler avec les blocs :

  • Le dernier bloc de premier niveau n'a pas besoin de balise fermante (le bloc se termine avec la fin du document). Cela simplifie l'écriture des templates enfants qui contiennent un bloc principal.
  • Pour une meilleure lisibilité, vous pouvez éventuellement indiquer le nom du bloc dans la balise {/block}, par exemple {/block footer}. Le nom doit toutefois correspondre à celui du bloc. Dans les gros templates, cette technique aide à voir quelles balises de bloc sont refermées.
  • Vous ne pouvez pas définir directement plusieurs balises de bloc du même nom dans le même template. Vous pouvez en revanche y parvenir avec les noms de blocs dynamiques.
  • Vous pouvez utiliser les n:attributs pour définir des blocs, comme <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Les blocs peuvent aussi s'utiliser sans nom, uniquement pour appliquer des filtres à la sortie : {block|strip} hello {/block}

Réutilisation horizontale {import}

La réutilisation horizontale est le troisième mécanisme de réutilisation et d'héritage de Latte. Elle permet de charger des blocs depuis d'autres templates. C'est comparable à la création d'un fichier de fonctions utilitaires en PHP, ensuite chargé avec require.

Si l'héritage de layout des templates est l'une des fonctionnalités les plus puissantes de Latte, il se limite à un héritage simple : un template ne peut étendre qu'un seul autre template. La réutilisation horizontale est un moyen d'obtenir un héritage multiple.

Prenons un fichier avec des définitions de blocs :

{block sidebar}...{/block}

{block menu}...{/block}

Avec la commande {import}, nous importons dans un autre template tous les blocs et définitions définis dans blocks.latte :

{import 'blocks.latte'}

{* les blocs sidebar et menu sont désormais utilisables *}

Si vous importez les blocs dans le template parent (c'est-à-dire si vous utilisez {import} dans layout.latte), les blocs seront aussi disponibles dans tous les templates enfants, ce qui est très pratique.

Le template destiné à être importé (par ex. blocks.latte) ne doit pas étendre un autre template, autrement dit utiliser {layout}. Il peut en revanche importer d'autres templates.

La balise {import} devrait être la première balise du template après {layout}. Le nom du template peut être n'importe quelle expression PHP :

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

Vous pouvez utiliser autant d'instructions {import} que vous voulez dans un template. Si deux templates importés définissent le même bloc, le premier l'emporte. Le template principal a toutefois la priorité la plus élevée et peut redéfinir n'importe quel bloc importé.

La balise {import} peut aussi passer des arguments au template importé, par exemple {import 'blocks.latte', foo: 1}. Ces arguments sont ensuite disponibles comme variables dans les blocs et définitions importés.

Le contenu des blocs écrasés peut être conservé en insérant le bloc de la même manière qu'un bloc parent :

{layout 'layout.latte'}

{import 'blocks.latte'}

{block sidebar}
	{include parent}
{/block}

{block title}...{/block}
{block content}...{/block}

Dans cet exemple, {include parent} appelle le bloc sidebar du template blocks.latte.

Héritage unitaire {embed}

L'héritage unitaire étend l'idée de l'héritage de layout au niveau des fragments de contenu. Là où l'héritage de layout travaille avec des “squelettes de documents” que les templates enfants viennent animer, l'héritage unitaire vous permet de créer des squelettes pour de plus petites unités de contenu et de les réutiliser où bon vous semble.

Dans l'héritage unitaire, la balise {embed} est la clé. Elle combine le comportement de {include} et de {layout}. Elle vous permet d'incorporer le contenu d'un autre template ou d'un bloc et, éventuellement, de lui passer des variables, exactement comme {include}. Elle permet aussi de redéfinir n'importe quel bloc défini dans le template incorporé, comme {layout}.

Prenons par exemple un élément accordéon. Voici le squelette de l'élément, stocké dans le template collapsible.latte :

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

	<div class="collapsible__content">
		{block content}{/block}
	</div>
</section>

Les balises {block} définissent deux blocs que les templates enfants peuvent remplir. Oui, exactement comme dans le cas du template parent en héritage de layout. Vous voyez aussi la variable $modifierClass.

Utilisons notre élément dans un template. C'est là qu'intervient {embed}. C'est une balise extrêmement puissante, qui nous permet de faire tout ceci : incorporer le contenu du template de l'élément, y ajouter des variables et y ajouter des blocs avec notre propre 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}

La sortie pourrait ressembler à ceci :

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

Les blocs à l'intérieur des balises embed forment une couche distincte, isolée des blocs extérieurs à l'embed. Ils peuvent donc porter le même nom qu'un bloc extérieur sans collision, et n'en sont pas affectés. À l'aide de la balise include à l'intérieur des balises {embed}, vous pouvez insérer les blocs créés ici, les blocs du template incorporé (ceux qui ne sont pas locaux), ainsi que les blocs du template principal qui sont locaux. Vous pouvez aussi importer des blocs depuis d'autres fichiers :

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

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

	{block inner}…{/block}

	{block title}
		{include inner} {* fonctionne, le bloc est défini dans l'embed *}
		{include hello} {* fonctionne, le bloc est local à ce template *}
		{include content} {* fonctionne, le bloc est défini dans le template incorporé *}
		{include aBlockDefinedInImportedTemplate} {* fonctionne *}
		{include outer} {* ne fonctionne pas ! - le bloc est dans la couche extérieure *}
	{/block}
{/embed}

Les templates incorporés n'ont pas accès aux variables du contexte actif, mais ils ont accès aux variables globales.

Avec {embed}, vous pouvez incorporer non seulement des templates, mais aussi d'autres blocs ; l'exemple précédent pourrait donc s'écrire ainsi :

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

Il y a toutefois une différence entre les deux : quand vous incorporez un bloc au lieu d'un fichier, les blocs de la couche extérieure restent accessibles à l'intérieur de l'embed. Contrairement au fichier incorporé, {include outer} y fonctionnerait donc.

Si nous passons une expression à {embed} et qu'on ne voit pas clairement s'il s'agit d'un nom de bloc ou d'un nom de fichier, ajoutez le mot-clé block ou file :

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

Cas d'utilisation

Latte propose plusieurs types d'héritage et de réutilisation de code. Récapitulons les principaux concepts pour y voir plus clair :

{include template}

Cas d'utilisation : utiliser header.latte et footer.latte dans 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}

Cas d'utilisation : étendre layout.latte dans homepage.latte et 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}

Cas d'utilisation : utiliser sidebar.latte dans single.product.latte et 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}

Cas d'utilisation : des fonctions qui reçoivent des variables et rendent quelque chose.

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}

Cas d'utilisation : incorporer pagination.latte dans product.table.latte et 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}
version: 3.x