Tout ce que vous avez toujours voulu savoir sur le groupement

Lorsque vous travaillez avec des données dans des templates, vous pouvez souvent rencontrer le besoin de les regrouper ou de les afficher spécifiquement selon certains critères. Latte propose plusieurs outils puissants à cet effet.

Le filtre et la fonction |group permettent un regroupement efficace des données selon un critère spécifié, le filtre |batch facilite la division des données en lots de taille fixe, et la balise {iterateWhile} offre la possibilité d'un contrôle plus complexe des boucles avec des conditions. Chacune de ces balises offre des options spécifiques pour travailler avec les données, ce qui en fait des outils indispensables pour l'affichage dynamique et structuré des informations dans les templates Latte.

Filtre et fonction group

Imaginez une table de base de données items avec des éléments divisés en catégories :

id categoryId name
1 1 Apple
2 1 Banana
3 2 PHP
4 3 Green
5 3 Red
6 3 Blue

Une simple liste de tous les éléments à l'aide d'un template Latte ressemblerait à ceci :

<ul>
{foreach $items as $item}
	<li>{$item->name}</li>
{/foreach}
</ul>

Cependant, si nous voulions que les éléments soient organisés en groupes par catégorie, nous devrions les diviser de manière à ce que chaque catégorie ait sa propre liste. Le résultat devrait alors ressembler à ceci :

<ul>
	<li>Apple</li>
	<li>Banana</li>
</ul>

<ul>
	<li>PHP</li>
</ul>

<ul>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

La tâche peut être résolue facilement et élégamment en utilisant |group. Comme paramètre, nous spécifions categoryId, ce qui signifie que les éléments seront divisés en tableaux plus petits en fonction de la valeur de $item->categoryId (si $item était un tableau, $item['categoryId'] serait utilisé) :

{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

Le filtre peut également être utilisé comme une fonction dans Latte, ce qui nous donne une syntaxe alternative : {foreach group($items, categoryId) ...}.

Si vous souhaitez regrouper des éléments selon des critères plus complexes, vous pouvez utiliser une fonction dans le paramètre du filtre. Par exemple, regrouper les éléments par la longueur de leur nom ressemblerait à ceci :

{foreach ($items|group: fn($item) => strlen($item->name)) as $items}
	...
{/foreach}

Il est important de noter que $categoryItems n'est pas un tableau ordinaire, mais un objet qui se comporte comme un itérateur. Pour accéder au premier élément du groupe, vous pouvez utiliser la fonction first().

Cette flexibilité dans le regroupement des données fait de group un outil exceptionnellement utile pour la présentation des données dans les templates Latte.

Boucles imbriquées

Imaginons que nous ayons une table de base de données avec une colonne supplémentaire subcategoryId, qui définit les sous-catégories des éléments individuels. Nous voulons afficher chaque catégorie principale dans une liste <ul> séparée et chaque sous-catégorie dans une liste imbriquée <ol> séparée :

{foreach ($items|group: categoryId) as $categoryItems}
	<ul>
		{foreach ($categoryItems|group: subcategoryId) as $subcategoryItems}
			<ol>
				{foreach $subcategoryItems as $item}
					<li>{$item->name}
				{/foreach}
			</ol>
		{/foreach}
	</ul>
{/foreach}

Connexion avec Nette Database

Montrons comment utiliser efficacement le regroupement de données en combinaison avec Nette Database. Supposons que nous travaillions avec la table items de l'exemple introductif, qui est liée via la colonne categoryId à cette table categories :

categoryId name
1 Fruits
2 Languages
3 Colors

Nous chargeons les données de la table items en utilisant Nette Database Explorer avec la commande $items = $db->table('items'). Lors de l'itération sur ces données, nous avons la possibilité d'accéder non seulement aux attributs comme $item->name et $item->categoryId, mais aussi, grâce à la liaison avec la table categories, à la ligne correspondante via $item->category. Cette liaison peut démontrer une utilisation intéressante :

{foreach ($items|group: category) as $category => $categoryItems}
	<h1>{$category->name}</h1>
	<ul>
		{foreach $categoryItems as $item}
			<li>{$item->name}</li>
		{/foreach}
	</ul>
{/foreach}

Dans ce cas, nous utilisons le filtre |group pour regrouper par la ligne liée $item->category, et non seulement par la colonne categoryId. Grâce à cela, la variable clé contient directement l'ActiveRow de la catégorie donnée, ce qui nous permet d'afficher directement son nom en utilisant {$category->name}. C'est un exemple pratique de la façon dont le regroupement peut clarifier les templates et faciliter le travail avec les données.

Filtre |batch

Le filtre permet de diviser une liste d'éléments en groupes avec un nombre prédéterminé d'éléments. Ce filtre est idéal pour les situations où vous souhaitez présenter des données en plusieurs petits groupes, par exemple pour une meilleure clarté ou une organisation visuelle sur la page.

Imaginons que nous ayons une liste d'éléments et que nous voulions les afficher dans des listes, où chacune contient au maximum trois éléments. L'utilisation du filtre |batch est très pratique dans un tel cas :

<ul>
{foreach ($items|batch: 3) as $batch}
	{foreach $batch as $item}
		<li>{$item->name}</li>
	{/foreach}
{/foreach}
</ul>

Dans cet exemple, la liste $items est divisée en groupes plus petits, où chaque groupe ($batch) contient jusqu'à trois éléments. Chaque groupe est ensuite affiché dans une liste <ul> séparée.

Si le dernier groupe ne contient pas suffisamment d'éléments pour atteindre le nombre souhaité, le deuxième paramètre du filtre permet de définir avec quoi ce groupe sera complété. C'est idéal pour l'alignement esthétique des éléments là où une ligne incomplète pourrait sembler désordonnée.

{foreach ($items|batch: 3, '—') as $batch}
	...
{/foreach}

Balise {iterateWhile}

Nous allons montrer les mêmes tâches que nous avons résolues avec le filtre |group, mais en utilisant la balise {iterateWhile}. La principale différence entre les deux approches est que group traite et regroupe d'abord toutes les données d'entrée, tandis que {iterateWhile} contrôle le déroulement des boucles avec des conditions, de sorte que l'itération se déroule progressivement.

D'abord, nous allons afficher le tableau avec les catégories en utilisant iterateWhile :

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}</li>
		{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
	</ul>
{/foreach}

Alors que {foreach} marque la partie externe de la boucle, c'est-à-dire l'affichage des listes pour chaque catégorie, la balise {iterateWhile} marque la partie interne, c'est-à-dire les éléments individuels. La condition dans la balise de fin indique que la répétition se poursuivra tant que l'élément actuel et le suivant appartiennent à la même catégorie ($iterator->nextValue est l'élément suivant).

Si la condition était toujours remplie, tous les éléments seraient affichés dans la boucle interne :

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<li>{$item->name}
		{/iterateWhile true}
	</ul>
{/foreach}

Le résultat ressemblera à ceci :

<ul>
	<li>Apple</li>
	<li>Banana</li>
	<li>PHP</li>
	<li>Green</li>
	<li>Red</li>
	<li>Blue</li>
</ul>

À quoi sert une telle utilisation d'iterateWhile ? Si le tableau est vide et ne contient aucun élément, le <ul></ul> vide ne sera pas affiché.

Si nous spécifions la condition dans la balise d'ouverture {iterateWhile}, le comportement change : la condition (et le passage à l'élément suivant) est exécutée au début de la boucle interne, et non à la fin. Ainsi, alors qu'on entre toujours dans {iterateWhile} sans condition, on entre dans {iterateWhile $cond} uniquement si la condition $cond est remplie. Et en même temps, l'élément suivant est écrit dans $item.

Ce qui est utile, par exemple, dans une situation où nous voulons afficher le premier élément de chaque catégorie d'une manière différente, comme ceci :

<h1>Apple</h1>
<ul>
	<li>Banana</li>
</ul>

<h1>PHP</h1>
<ul>
</ul>

<h1>Green</h1>
<ul>
	<li>Red</li>
	<li>Blue</li>
</ul>

Nous modifions le code original de manière à afficher d'abord le premier élément, puis dans la boucle interne {iterateWhile}, nous affichons les autres éléments de la même catégorie :

{foreach $items as $item}
	<h1>{$item->name}</h1>
	<ul>
		{iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
			<li>{$item->name}</li>
		{/iterateWhile}
	</ul>
{/foreach}

Au sein d'une même boucle, nous pouvons créer plusieurs boucles internes et même les imbriquer. De cette manière, on pourrait regrouper les sous-catégories, etc.

Supposons qu'il y ait une autre colonne subcategoryId dans la table et qu'en plus de chaque catégorie dans un <ul> séparé, chaque sous-catégorie soit dans un <ol> séparé :

{foreach $items as $item}
	<ul>
		{iterateWhile}
			<ol>
				{iterateWhile}
					<li>{$item->name}
				{/iterateWhile $item->subcategoryId === $iterator->nextValue->subcategoryId}
			</ol>
		{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
	</ul>
{/foreach}
version: 3.0