Héritage et réutilisabilité des templates

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

Le concept d'héritage de template Latte est similaire à l'héritage de classe en PHP. Vous définissez un template parent, dont d'autres templates enfants peuvent hériter et peuvent remplacer des parties du template parent. Cela fonctionne très bien lorsque les éléments partagent une structure commune. Cela semble compliqué ? Ne vous inquiétez pas, c'est très facile.

Héritage de layout {layout}

Voyons l'héritage de template de layout, c'est-à-dire la mise en page, avec un exemple. Ceci est le template parent, que nous appellerons par exemple layout.latte, et qui définit le squelette du 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. La balise block ne fait qu'indiquer que cet emplacement peut être remplacé par un template enfant en définissant son propre bloc avec le même nom.

Un template enfant peut ressembler à ceci :

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

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

À ce stade, Latte remarque les trois balises block dans layout.latte et remplace ces blocs par le contenu du template enfant. Étant donné que le template enfant n'a pas défini de bloc footer, le contenu du template parent est utilisé à la place. Le contenu de la balise {block} dans le template parent est toujours utilisé comme solution de secours.

La sortie peut 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 le template enfant, les blocs ne peuvent être placés qu'au niveau supérieur ou à l'intérieur d'un autre bloc, c'est-à-dire :

{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 comme vraie ou fausse. Donc, même si cela ne semble pas être le cas, ce template définira le bloc.

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

Si vous souhaitez que la sortie à l'intérieur du bloc s'affiche conditionnellement, utilisez plutôt ce qui suit :

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

L'espace en dehors des blocs dans le template enfant est exécuté avant le rendu du template de layout, vous pouvez donc l'utiliser pour définir des variables comme {var $foo = bar} et pour propager des données à travers toute la chaîne d'héritage :

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

...

Héritage à plusieurs niveaux

Vous pouvez utiliser autant de niveaux d'héritage que nécessaire. Une manière courante d'utiliser l'héritage de layout est l'approche à trois niveaux suivante :

  1. Créez un template layout.latte qui contient le squelette principal de l'apparence du site.
  2. Créez un template layout-SECTIONNAME.latte pour chaque section de votre site. Par exemple, layout-news.latte, layout-blog.latte, etc. Tous ces templates étendent layout.latte et incluent les styles & design spécifiques à chaque section.
  3. Créez des templates individuels pour chaque type de page, par exemple un article de journal ou une entrée de blog. Ces templates étendent le template de section approprié.

Héritage dynamique

Le nom du template parent peut être une variable ou toute expression PHP, de sorte que l'héritage peut se comporter de manière dynamique :

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

Vous pouvez également utiliser l'API Latte pour sélectionner automatiquement le template de layout.

Conseils

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

  • Si vous utilisez {layout} dans un template, ce doit être la première balise du template.
  • Le layout peut être recherché automatiquement (comme par exemple dans les presenters). Dans ce cas, si le template ne doit pas avoir de layout, il l'indique avec la balise {layout none}.
  • La balise {layout} a un alias {extends}.
  • Le nom du fichier de layout dépend du chargeur.
  • Vous pouvez avoir autant de blocs que vous le souhaitez. N'oubliez pas que les templates enfants n'ont pas besoin de définir tous les blocs parents, vous pouvez donc remplir des valeurs par défaut raisonnables dans plusieurs blocs, puis ne définir que ceux dont vous avez besoin plus tard.

Blocs {block}

Voir aussi le {block} anonyme

Un bloc représente une manière de modifier la façon dont une certaine partie du template est rendue, mais n'interfère en aucune façon avec la logique qui l'entoure. Dans l'exemple suivant, nous montrerons comment un bloc fonctionne, mais aussi comment il ne fonctionne pas :

{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 externes. Ils donnent simplement la possibilité d'être remplacés par un template enfant :

{layout 'parent.Latte'}

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

Maintenant, lors du rendu du template enfant, la boucle utilisera le bloc défini dans le template enfant child.Latte au lieu du bloc défini dans parent.Latte; le template exécuté est alors équivalent à ce qui suit :

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

Cependant, 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 du bloc peut être modifié à l'aide de filtres. L'exemple suivant supprime tout le HTML et modifie la casse :

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

La balise peut également être écrite comme un n:attribut :

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

Blocs locaux

Chaque bloc remplace le contenu du bloc parent du même nom – à l'exception des blocs locaux. Dans les classes, ce serait quelque chose comme des méthodes privées. Vous pouvez ainsi créer un template sans craindre que, en raison de la correspondance des noms de blocs, ils soient remplacés par un autre template.

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

Rendu des blocs {include}

Voir aussi {include file}

Pour afficher un bloc à un endroit spécifique, utilisez la balise {include blockname} :

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

<h1>{include title}</h1>

Il est également possible d'afficher un bloc d'un autre template :

{include footer from 'main.latte'}

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

Vous pouvez passer des variables au bloc de cette manière :

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

Le nom du bloc peut être une variable ou toute expression PHP. Dans ce cas, nous ajoutons le mot-clé block avant la variable pour que Latte sache déjà au moment de la compilation qu'il s'agit d'un bloc et non d'une inclusion de template, dont le nom pourrait également être dans une variable :

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

Un bloc peut ê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 alors écrire {include this, ...}, où this signifie le bloc actuel.

Le bloc rendu peut être modifié à l'aide de filtres. L'exemple suivant supprime tout le HTML et modifie la casse :

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

Bloc parent

Si vous avez besoin d'afficher le contenu d'un bloc du template parent, utilisez {include parent}. C'est utile si vous souhaitez simplement compléter le contenu du bloc parent au lieu de le remplacer complètement.

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

Définitions {define}

En plus des blocs, Latte propose également des “définitions”. Dans les langages de programmation courants, nous les comparerions à des fonctions. Elles sont utiles pour réutiliser des fragments de template afin de ne pas vous répéter.

Latte essaie de simplifier les choses, donc en gros, les définitions sont identiques aux blocs et tout ce qui est dit sur les blocs s'applique également aux définitions. Elles diffèrent des blocs en ce que :

  1. elles sont enfermées dans des balises {define}
  2. elles ne sont rendues que lorsque vous les incluez via {include}
  3. on peut leur définir des paramètres de la même manière que 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 d'aide avec une collection de définitions sur la façon de 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 une valeur par défaut de null, sauf si une valeur par défaut est spécifiée (ici 'text' est la valeur par défaut pour $type). Les types de paramètres peuvent également être déclarés : {define input, string $name, ...}.

Nous chargeons le template avec les définitions à l'aide de {import}. Les définitions elles-mêmes sont rendues de la même manière que les blocs :

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

Les définitions n'ont pas accès aux variables du contexte actif, mais elles ont accès aux variables globales.

Noms de blocs dynamiques

Latte permet une grande flexibilité dans la définition des blocs, car le nom du 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 alors redéfinir, par exemple, un seul bloc :

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

Ainsi, la sortie ressemblera à ceci :

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

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

Voir aussi {ifset $var}

À l'aide du test {ifset blockname}, nous vérifions si un bloc (ou plusieurs blocs) existe dans le contexte actuel :

{ifset footer}
	...
{/ifset}

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

Le nom du bloc peut être une variable ou toute expression PHP. Dans ce cas, nous ajoutons le mot-clé block avant la variable pour qu'il soit clair 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 niveau supérieur n'a pas besoin d'avoir une balise de fermeture (le bloc se termine à la fin du document). Cela simplifie l'écriture des templates enfants qui contiennent un bloc principal unique.
  • Pour une meilleure lisibilité, vous pouvez indiquer le nom du bloc dans la balise {/block}, par exemple {/block footer}. Cependant, le nom doit correspondre au nom du bloc. Dans les templates plus volumineux, cette technique vous aidera à voir quelles balises de bloc se ferment.
  • Vous ne pouvez pas définir directement plusieurs balises de bloc avec le même nom dans le même template. Cependant, cela peut être réalisé en utilisant des noms de blocs dynamiques.
  • Vous pouvez utiliser des n:attributs pour définir des blocs comme <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Les blocs peuvent également être utilisés sans nom uniquement pour appliquer des filtres{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 dans Latte. Elle permet de charger des blocs à partir d'autres templates. C'est similaire à la création d'un fichier avec des fonctions d'aide en PHP, que nous chargeons ensuite à l'aide de require.

Bien que l'héritage de layout de template soit l'une des fonctionnalités les plus puissantes de Latte, il est limité à l'héritage simple – un template ne peut étendre qu'un seul autre template. La réutilisation horizontale est un moyen d'atteindre l'héritage multiple.

Ayons un fichier avec des définitions de blocs :

{block sidebar}...{/block}

{block menu}...{/block}

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

{import 'blocks.latte'}

{* maintenant les blocs sidebar et menu peuvent être utilisés *}

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

Le template destiné à être importé (par exemple blocks.latte) ne doit pas étendre un autre template, c'est-à-dire utiliser {layout}. Cependant, il peut importer d'autres templates.

La balise {import} doit ê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 de commandes {import} que vous le souhaitez dans un template. Si deux templates importés définissent le même bloc, le premier l'emporte. Cependant, le template principal a la priorité la plus élevée et peut remplacer n'importe quel bloc importé.

Le contenu des blocs remplacés peut être préservé en insérant le bloc de la même manière que le bloc parent est inséré :

{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. Alors que l'héritage de layout fonctionne avec le “squelette du document”, qui est animé par les templates enfants, l'héritage unitaire vous permet de créer des squelettes pour des unités de contenu plus petites et de les réutiliser où vous le souhaitez.

Dans l'héritage unitaire, la clé est la balise {embed}. Elle combine le comportement de {include} et {layout}. Elle permet d'insérer le contenu d'un autre template ou bloc et de passer éventuellement des variables, tout comme avec {include}. Elle permet également de remplacer n'importe quel bloc défini à l'intérieur du template inséré, comme lors de l'utilisation de {layout}.

Par exemple, utilisons un élément accordéon. Regardons 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, comme dans le cas du template parent dans l'héritage de layout. Vous voyez également 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 toutes ces choses : insérer 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 peut 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 insérées forment une couche distincte indépendante des autres blocs. Par conséquent, ils peuvent avoir le même nom qu'un bloc en dehors de l'insertion et ne sont en aucun cas affectés. En utilisant la balise include à l'intérieur des balises {embed}, vous pouvez insérer les blocs créés ici, les blocs du template inséré (qui ne sont pas locaux) ainsi que les blocs du template principal qui, au contraire, sont locaux. Vous pouvez également importer des blocs à partir 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 à l'intérieur de embed *}
		{include hello} {* fonctionne, le bloc est local dans ce template *}
		{include content} {* fonctionne, le bloc est défini dans le template inséré *}
		{include aBlockDefinedInImportedTemplate} {* fonctionne *}
		{include outer} {* ne fonctionne pas ! - le bloc est dans la couche externe *}
	{/block}
{/embed}

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

Avec {embed}, on peut insérer non seulement des templates, mais aussi d'autres blocs, et donc l'exemple précédent pourrait être écrit de cette manière :

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

Si nous passons une expression à {embed} et qu'il n'est pas clair s'il s'agit d'un nom de bloc ou de fichier, nous ajoutons le mot-clé block ou file :

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

Cas d'utilisation

Dans Latte, il existe différents types d'héritage et de réutilisation de code. Résumons les concepts principaux pour une meilleure clarté :

{include template}

Cas d'utilisation: Utilisation de header.latte et footer.latte à l'intérieur de 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: Extension de layout.latte à l'intérieur de 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: 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: Fonctions auxquelles on passe des variables et qui affichent 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: Insertion de 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.0