Todo lo que siempre quisiste saber sobre la agrupación

Al trabajar con datos en plantillas, a menudo puedes encontrar la necesidad de agruparlos o mostrarlos de forma específica según ciertos criterios. Latte ofrece varias herramientas potentes para este propósito.

El filtro y la función |group permiten una agrupación eficiente de datos según un criterio especificado, el filtro |batch facilita la división de datos en lotes de tamaño fijo y la etiqueta {iterateWhile} proporciona la capacidad de controlar de forma más compleja el flujo de los bucles mediante condiciones. Cada una de estas características ofrece posibilidades específicas para trabajar con datos, lo que las convierte en herramientas indispensables para la visualización dinámica y estructurada de información en las plantillas Latte.

Filtro y función group

Imagina una tabla de base de datos items con elementos divididos en categorías:

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

Una lista simple de todos los elementos usando una plantilla Latte se vería así:

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

Sin embargo, si quisiéramos que los elementos estuvieran organizados en grupos por categoría, necesitaríamos dividirlos de modo que cada categoría tuviera su propia lista. El resultado debería verse así:

<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 tarea se puede resolver fácil y elegantemente usando |group. Como parámetro, especificamos categoryId, lo que significa que los elementos se dividirán en arrays más pequeños según el valor de $item->categoryId (si $item fuera un array, se usaría $item['categoryId']):

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

El filtro también se puede usar en Latte como una función, lo que nos da una sintaxis alternativa: {foreach group($items, categoryId) ...}.

Si deseas agrupar elementos según criterios más complejos, puedes usar una función en el parámetro del filtro. Por ejemplo, agrupar elementos por la longitud del nombre se vería así:

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

Es importante tener en cuenta que $categoryItems no es un array común, sino un objeto que se comporta como un iterador. Para acceder al primer elemento del grupo, puedes usar la función first().

Esta flexibilidad en la agrupación de datos hace que group sea una herramienta excepcionalmente útil para presentar datos en plantillas Latte.

Bucles anidados

Imaginemos que tenemos una tabla de base de datos con otra columna subcategoryId, que define las subcategorías de los elementos individuales. Queremos mostrar cada categoría principal en una lista <ul> separada y cada subcategoría en una lista <ol> anidada separada:

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

Conexión con Nette Database

Veamos cómo utilizar eficazmente la agrupación de datos en combinación con Nette Database. Supongamos que estamos trabajando con la tabla items del ejemplo introductorio, que está conectada a través de la columna categoryId con esta tabla categories:

categoryId name
1 Fruits
2 Languages
3 Colors

Cargamos los datos de la tabla items usando Nette Database Explorer con el comando $items = $db->table('items');. Durante la iteración sobre estos datos, tenemos la posibilidad de acceder no solo a atributos como $item->name y $item->categoryId, sino también, gracias a la conexión con la tabla categories, a la fila relacionada en ella a través de $item->category. Esta conexión permite un uso interesante:

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

En este caso, usamos el filtro |group para agrupar según la fila conectada $item->category, no solo según la columna categoryId. Gracias a esto, en la variable clave tenemos directamente el ActiveRow de la categoría dada, lo que nos permite imprimir directamente su nombre usando {$category->name}. Este es un ejemplo práctico de cómo la agrupación puede aclarar las plantillas y facilitar el trabajo con los datos.

Filtro |batch

El filtro permite dividir una lista de elementos en grupos con un número predeterminado de elementos. Este filtro es ideal para situaciones en las que deseas presentar datos en varios grupos más pequeños, por ejemplo, para una mejor claridad u organización visual en la página.

Imaginemos que tenemos una lista de elementos y queremos mostrarlos en listas donde cada una contenga como máximo tres elementos. El uso del filtro |batch es muy práctico en tal caso:

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

En este ejemplo, la lista $items se divide en grupos más pequeños, donde cada grupo ($batch) contiene hasta tres elementos. Cada grupo se muestra luego en una lista <ul> separada.

Si el último grupo no contiene suficientes elementos para alcanzar el número deseado, el segundo parámetro del filtro permite definir con qué se rellenará este grupo. Esto es ideal para alinear estéticamente los elementos donde una fila incompleta podría parecer desordenada.

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

Etiqueta {iterateWhile}

Las mismas tareas que resolvimos con el filtro |group, las mostraremos usando la etiqueta {iterateWhile}. La principal diferencia entre ambos enfoques es que group primero procesa y agrupa todos los datos de entrada, mientras que {iterateWhile} controla el flujo del bucle mediante condiciones, por lo que la iteración se realiza de forma progresiva.

Primero, renderizamos la tabla con categorías usando iterateWhile:

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

Mientras que {foreach} marca la parte externa del bucle (la renderización de listas para cada categoría), la etiqueta {iterateWhile} marca la parte interna (los elementos individuales). La condición en la etiqueta de cierre indica que la repetición continuará mientras el elemento actual y el siguiente pertenezcan a la misma categoría ($iterator->nextValue es el elemento siguiente).

Si la condición se cumpliera siempre, todos los elementos se renderizarían en el bucle interno:

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

El resultado se verá así:

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

¿Para qué sirve este uso de iterateWhile? Cuando la tabla esté vacía y no contenga ningún elemento, no se imprimirá un <ul></ul> vacío.

Si especificamos la condición en la etiqueta de apertura {iterateWhile}, el comportamiento cambia: la condición (y el paso al siguiente elemento) se evalúa al principio del bucle interno, no al final. Por lo tanto, mientras que siempre se entra en {iterateWhile} sin condición, solo se entra en {iterateWhile $cond} si se cumple la condición $cond. Y al mismo tiempo, el siguiente elemento se asigna a $item.

Esto es útil, por ejemplo, en una situación en la que queramos renderizar el primer elemento de cada categoría de manera diferente, como en este ejemplo:

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

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

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

Modificamos el código original para que primero renderice el primer elemento y luego, en el bucle interno {iterateWhile}, renderice los demás elementos de la misma categoría:

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

Dentro de un mismo bucle {foreach}, podemos crear múltiples bucles internos {iterateWhile} e incluso anidarlos. De esta manera, se podrían agrupar, por ejemplo, subcategorías.

Supongamos que en la tabla hay otra columna subcategoryId y, además de que cada categoría esté en un <ul> separado, cada subcategoría debe estar en un <ol> separado:

{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}
versión: 3.0