Héritage et réutilisabilité des modèles

Les mécanismes de réutilisation et d'héritage des modèles sont là pour booster votre productivité car chaque modèle ne contient que son contenu unique et les éléments et structures répétés sont réutilisés. Nous présentons trois concepts : l'héritage des modèles, la réutilisation horizontale et l'héritage des unités.

Le concept d'héritage des modèles Latte est similaire à l'héritage des classes PHP. Vous définissez un modèle parent à partir duquel les autres modèles enfants peuvent s'étendre et remplacer certaines parties du modèle parent. Ce concept fonctionne parfaitement lorsque les éléments partagent une structure commune. Cela vous semble compliqué ? Ne vous inquiétez pas, ce n'est pas le cas.

Héritage de la mise en page {layout}

Examinons l'héritage des modèles de mise en page en commençant par un exemple. Il s'agit d'un modèle parent que nous appellerons par exemple layout.latte et qui définit un squelette de 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>

La balise {block} définit trois blocs que les modèles enfants peuvent remplir. La balise block indique au moteur de modèle qu'un modèle enfant peut remplacer ces parties du modèle en définissant son propre bloc du même nom.

Un modèle enfant peut 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 la clé ici. Elle indique au moteur de modèle que ce modèle “étend” un autre modèle. Lorsque Latte rend ce modèle, il localise d'abord le parent – dans ce cas, layout.latte.

À ce stade, le moteur de modèle remarque les trois balises de bloc dans layout.latte et remplace ces blocs par le contenu du modèle enfant. Notez que, comme le modèle enfant n'a pas défini le bloc footer, le contenu du modèle parent est utilisé à la place. Le contenu d'une balise {block} dans un modèle parent est toujours utilisé comme solution de repli.

Le résultat 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 un modèle enfant, les blocs ne peuvent être situés qu'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éé dans le modèle, que la condition environnante {if} soit évaluée comme étant vraie ou fausse. Contrairement à ce que vous pourriez penser, ce modèle définit bien un bloc.

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

Si vous souhaitez que la sortie à l'intérieur du bloc soit affichée de manière conditionnelle, utilisez plutôt ce qui suit :

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

Les données situées à l'extérieur d'un bloc dans un modèle enfant sont exécutées avant que le modèle de présentation ne soit rendu. Vous pouvez donc l'utiliser pour définir des variables telles que {var $foo = bar} et propager les données à l'ensemble de la chaîne d'héritage :

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

...

Héritage multi-niveaux

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

  1. Créez un modèle layout.latte qui contient l'aspect et la convivialité principaux de votre site.
  2. Créez un modèle layout-SECTIONNAME.latte pour chaque section de votre site. Par exemple, layout-news.latte, layout-blog.latte etc. Ces modèles étendent tous layout.latte et incluent des styles/conceptions spécifiques à chaque section.
  3. Créez des modèles individuels pour chaque type de page, par exemple un article d'actualité ou une entrée de blog. Ces modèles étendent le modèle de section approprié.

Héritage dynamique de la mise en page

Vous pouvez utiliser une variable ou toute expression PHP comme nom du modèle parent, 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 choisir automatiquement le modèle de mise en page.

Conseils

Voici quelques conseils pour travailler avec l'héritage de mise en page :

  • Si vous utilisez {layout} dans un modèle, il doit s'agir de la première balise de modèle de ce modèle.
  • La mise en page peut être recherchée automatiquement (comme dans les présentateurs). Dans ce cas, si le modèle ne doit pas avoir de mise en page, il l'indiquera avec la balise {layout none}.
  • La balise {layout} a un alias {extends}.
  • Le nom de fichier du modèle étendu dépend du chargeur de modèle.
  • Vous pouvez avoir autant de blocs que vous le souhaitez. Rappelez-vous que les modèles enfants n'ont pas à définir tous les blocs parents, vous pouvez donc remplir des valeurs par défaut raisonnables dans un certain nombre de blocs, puis ne définir que ceux dont vous avez besoin plus tard.

Blocs {block}

Voir aussi anonyme {block}

Un bloc permet de modifier la façon dont une certaine partie d'un modèle est rendue, mais il n'interfère en aucune façon avec la logique qui l'entoure. Prenons l'exemple suivant pour illustrer comment un bloc fonctionne et, surtout, 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 modèle, le résultat sera exactement le même avec ou sans les balises de bloc. Les blocs ont accès aux variables des scopes externes. Il s'agit simplement d'un moyen de les rendre surmontables par un modèle enfant :

{layout 'parent.Latte'}

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

Maintenant, lors du rendu du modèle enfant, la boucle va utiliser le bloc défini dans le modèle enfant child.Latte au lieu de celui défini dans le modèle de base parent.Latte; le modèle exécuté est alors équivalent au modèle suivant :

{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 si nous remplaçons une valeur d'une variable existante, le changement sera visible uniquement à l'intérieur du bloc :

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

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

Le contenu du bloc peut être modifié par des filtres. L'exemple suivant supprime tout le HTML et met le titre en majuscule :

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

La balise peut aussi être écrite comme n:attribute:

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

Blocs locaux

Chaque bloc remplace le contenu du bloc parent du même nom. Sauf pour les blocs locaux. Ils sont un peu comme les méthodes privées d'une classe. Vous pouvez créer un modèle sans craindre que – en raison de la coïncidence des noms des blocs – ils soient écrasés par le second modèle.

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

Blocs d'impression {include}

Voir aussi {include file}

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

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

<h1>{include title}</h1>

Vous pouvez également afficher le bloc à partir d'un autre modèle :

{include footer from 'main.latte'}

Les blocs imprimés n'ont 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, ils ont accès aux variables globales.

Vous pouvez transmettre des variables au bloc de la manière suivante :

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

Vous pouvez utiliser une variable ou toute expression en PHP comme nom de bloc. Dans ce cas, ajoutez le mot-clé block devant la variable, afin que l'on sache à la compilation qu'il s'agit d'un bloc, et non d'un modèle d'insertion, dont le nom pourrait également se trouver dans la variable :

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

Le bloc peut également être imprimé à l'intérieur de lui-même, ce qui est utile, par exemple, lors du rendu d'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 également écrire {include this, ...}this signifie le bloc actuel.

Le contenu imprimé peut être modifié par des filtres. L'exemple suivant supprime tout le HTML et met le titre en majuscule :

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

Bloc parent

Si vous devez imprimer le contenu du bloc à partir du modèle parent, l'instruction {include parent} fera l'affaire. Elle est utile si vous souhaitez compléter le contenu d'un 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, il existe également des “définitions” dans Latte. Elles sont comparables aux fonctions dans les langages de programmation ordinaires. Elles sont utiles pour réutiliser des fragments de modèles afin de ne pas se répéter.

Latte essaie de garder les choses simples, donc les définitions sont les mêmes que les blocs, et tout ce qui est dit à propos des blocs s'applique également aux définitions. Elles diffèrent des blocs en ce sens que

  1. elles sont entourées de balises {define}
  2. elles ne sont rendues que lorsqu'elles sont insérées par l'intermédiaire d'une balise {include}
  3. elles peuvent être paramétrées comme des fonctions en 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> *}

Imaginez que vous disposiez d'un modèle d'aide contenant un ensemble de définitions sur la manière 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 d'une définition sont toujours facultatifs et ont une valeur par défaut : null, sauf si une valeur par défaut est spécifiée (ici, 'text' est la valeur par défaut de $type). Les types de paramètres peuvent également être déclarés : {define input, string $name, ...}.

Le modèle contenant les définitions est chargé à l'aide de la commande {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 [Pierre, Jean, Marie] as $name}
	{block "hi-$name"}Hi, je suis {$name}.{/block}
{/foreach}

Par exemple, nous ne pouvons redéfinir qu'un seul bloc dans un modèle enfant :

{block hi-Jean}Bonjour. Je suis {$name}.{/block}

Ainsi, la sortie ressemblera à ceci :

Hi, I am Pierre.
Hello. I am Jean.
Hi, I am Marie.

Checking Block Existence {ifset}

Voir aussi {ifset $var}

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

{ifset footer}
	...
{/ifset}

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

Vous pouvez utiliser une variable ou toute expression en PHP comme nom de bloc. Dans ce cas, ajoutez le mot-clé block devant la variable pour indiquer clairement que ce n'est pas la variable qui est vérifiée :

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

L'existence de blocs est également renvoyée par la fonction hasBlock():

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

Conseils

Voici quelques conseils pour travailler avec des blocs :

  • Le dernier bloc de premier niveau n'a pas besoin d'avoir de balise de fermeture (le bloc se termine à la fin du document). Cela simplifie l'écriture des modèles enfants, qui n'ont qu'un seul bloc primaire.
  • Pour plus de lisibilité, vous pouvez éventuellement donner un nom à votre balise {/block}, par exemple {/block footer}. Toutefois, ce nom doit correspondre au nom du bloc. Dans les modèles de grande taille, cette technique vous aide à voir quelles balises de bloc sont fermées.
  • Vous ne pouvez pas définir directement plusieurs balises de bloc portant le même nom dans un même modèle. Mais vous pouvez y parvenir en utilisant des noms de blocs dynamiques.
  • Vous pouvez utiliser n:attributes pour définir des blocs tels que <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Les blocs peuvent également être utilisés sans nom uniquement pour appliquer les 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 dans Latte. Il permet de charger des blocs à partir d'autres modèles. Il est similaire à la création d'un fichier avec des fonctions d'aide en PHP, puis à son chargement à l'aide de require.

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

Prenons un ensemble de définitions de blocs :

{block sidebar}...{/block}

{block menu}...{/block}

À l'aide de la commande {import}, importez tous les blocs et définitions définis dans blocks.latte dans un autre modèle :

{import 'blocks.latte'}

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

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

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

La balise {import} doit être la première balise de modèle après {layout}. Le nom du modèle peut être une expression PHP quelconque :

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

Vous pouvez utiliser autant d'instructions {import} que vous le souhaitez dans un modèle donné. Si deux modèles importés définissent le même bloc, le premier l'emporte. Toutefois, la plus haute priorité est accordée au modèle principal, qui peut écraser tout bloc importé.

Le contenu des blocs écrasés peut être préservé 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} appellera correctement le bloc sidebar à partir du modèle blocks.latte.

Héritage des unités {embed}

L'héritage des unités reprend l'idée de l'héritage des mises en page au niveau des fragments de contenu. Alors que l'héritage de la mise en page fonctionne avec des “squelettes de documents”, qui prennent vie grâce à des modèles enfants, l'héritage des unités vous permet de créer des squelettes pour de plus petites unités de contenu et de les réutiliser où vous le souhaitez.

Dans l'héritage d'unités, la balise {embed} est la clé. Elle combine le comportement de {include} et {layout}. Elle vous permet d'inclure le contenu d'un autre modèle ou bloc et de transmettre éventuellement des variables, comme le fait {include}. Elle vous permet également de remplacer tout bloc défini à l'intérieur du modèle inclus, comme le fait {layout}.

Par exemple, nous allons utiliser l'élément accordéon pliable. Jetons un coup d'œil au squelette de l'élément dans le modèle 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 modèles enfants peuvent remplir. Oui, comme dans le cas du modèle parent dans le modèle d'héritage de mise en page. Vous voyez aussi la variable $modifierClass.

Utilisons notre élément dans le modèle. C'est là que {embed} entre en jeu. Il s'agit d'un kit super puissant qui nous permet de tout faire : inclure le contenu du modèle de l'élément, y ajouter des variables et y ajouter des blocs avec du HTML personnalisé :

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

Le résultat pourrait ressembler à ça :

<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 d'intégration forment une couche distincte indépendante des autres blocs. Par conséquent, ils peuvent avoir le même nom que le bloc à l'extérieur de la balise embed et ne sont en aucun cas affectés. En utilisant la balise include à l'intérieur des balises {embed}, vous pouvez insérer des blocs créés ici, des blocs du modèle incorporé (qui ne sont pas locaux), ainsi que des blocs du modèle principal qui 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 dans l'inclusion *}
		{include hello} {* fonctionne, le bloc est local dans ce modèle *}
		{include content} {* fonctionne, le bloc est défini dans le modèle intégré *}
		{include aBlockDefinedInImportedTemplate} {* fonctionne *}
		{include outer} {* ne fonctionne pas ! - le bloc est dans la couche externe *}
	{/block}
{/embed}

Les modèles intégrés n'ont pas accès aux variables du contexte actif, mais ils ont accès aux variables globales.

Avec {embed} vous pouvez insérer non seulement des modèles mais aussi d'autres blocs, ainsi l'exemple précédent pourrait être écrit comme ceci :

{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 bloc ou d'un nom de fichier, ajoutez le mot-clé block ou file:

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

Cas d'utilisation

Il existe différents types d'héritage et de réutilisation du code dans Latte. Résumons les principaux concepts pour plus de clarté :

{include template}

Use Case: Utilisation de header.latte & 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 : Extension de layout.latte à l'intérieur de 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}

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

Cas d'utilisation : Une fonction qui récupère des variables et produit des balises.

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 : Incorporation de pagination.latte dans 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}
version: 3.0