Todo lo que siempre quiso saber sobre la agrupación

Al trabajar con datos en plantillas, a menudo se encuentra con la necesidad de agruparlos o mostrarlos específicamente según ciertos criterios. Para ello, Latte ofrece varias herramientas potentes.

El filtro y la función |group permiten agrupar eficazmente los datos en función de criterios especificados, mientras que el filtro |batch facilita la división de los datos en lotes fijos y la etiqueta {iterateWhile} ofrece la posibilidad de un control de ciclos más complejo con condiciones. Cada una de estas etiquetas ofrece opciones específicas para trabajar con los datos, lo que las convierte en herramientas indispensables para la visualización dinámica y estructurada de la información en las plantillas Latte.

Filtro y función group

Imagine una tabla de base de datos items con artículos divididos en categorías:

id categoryId name
1 Manzana    
2 1 Plátano    
3. 2. PHP    
4 3 Verde    
5. 3. Rojo    
6 3 Azul    

Una lista simple de todos los elementos utilizando una plantilla Latte tendría este aspecto:

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

Sin embargo, si quisiéramos que los artículos estuvieran organizados en grupos por categorías, tendríamos que dividirlos de forma que cada categoría tuviera su propia lista. El resultado sería el siguiente

<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 puede resolverse fácil y elegantemente utilizando |group. Especificamos categoryId como parámetro, lo que significa que los elementos se dividirán en matrices más pequeñas en función del valor de $item->categoryId (si $item fuera una matriz, utilizaríamos $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 utilizar como una función en Latte, lo que nos da una sintaxis alternativa: {foreach group($items, categoryId) ...}.

Si desea agrupar elementos según criterios más complejos, puede utilizar una función en el parámetro filtro. Por ejemplo, agrupar elementos por la longitud de su nombre tendría este aspecto:

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

Es importante tener en cuenta que $categoryItems no es una matriz normal, sino un objeto que se comporta como un iterador. Para acceder al primer elemento del grupo, puede utilizar la función first() para acceder al primer elemento del grupo.

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

Bucles anidados

Supongamos que tenemos una tabla de base de datos con otra columna subcategoryId que define subcategorías para cada artículo. Queremos mostrar cada categoría principal en una lista <ul> y cada subcategoría en una lista anidada separada <ol> anidada:

{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 la base de datos Nette

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 inicial, que está conectada a través de la columna categoryId a esta tabla categories:

categoryId name
1 Frutas  
2 Idiomas  
3 Colores  

Cargamos los datos de la tabla items utilizando el comando de Nette Database Explorer $items = $db->table('items'). Durante la iteración sobre estos datos, tenemos la oportunidad no sólo de acceder a atributos como $item->name y $item->categoryId, sino gracias a la conexión con la tabla categories, también a la fila relacionada en ella a través de $item->category. Esta conexión puede demostrar usos interesantes:

{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, utilizamos el filtro |group para agrupar por la fila conectada $item->category, no sólo por la columna categoryId. Esto nos da la ActiveRow de la categoría dada en la clave variable, permitiéndonos mostrar directamente su nombre usando {$category->name}. Este es un ejemplo práctico de cómo la agrupación puede simplificar las plantillas y facilitar el manejo de 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 desee presentar los datos en varios grupos más pequeños, por ejemplo, para una mayor claridad u organización visual en la página.

Imaginemos que tenemos una lista de elementos y queremos mostrarlos en listas, cada una de las cuales contiene un máximo de tres elementos. Utilizar el filtro |batch es muy práctico en un caso así:

<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, cada uno de los cuales ($batch) contiene un máximo de tres elementos. Cada grupo se muestra 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 completará este grupo. Esto es ideal para alinear estéticamente elementos en los que una fila incompleta podría parecer desordenada.

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

Etiqueta {iterateWhile}

Demostraremos las mismas tareas que abordamos con el filtro |group utilizando la etiqueta {iterateWhile}. La principal diferencia entre los dos enfoques es que group primero procesa y agrupa todos los datos de entrada, mientras que {iterateWhile} controla el progreso de los ciclos con condiciones, por lo que la iteración se produce secuencialmente.

En primer lugar, dibujamos una tabla con categorías utilizando 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 exterior del ciclo, es decir, dibujar listas para cada categoría, la etiqueta {iterateWhile} marca la parte interior, es decir, los elementos individuales. La condición en la etiqueta final dice que la repetición continuará mientras el elemento actual y el siguiente pertenezcan a la misma categoría ($iterator->nextValue es el siguiente elemento).

Si la condición se cumpliera siempre, todos los elementos se dibujarían en el ciclo interior:

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

El resultado tendrá este aspecto:

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

¿Para qué sirve iterateWhile de esta manera? Cuando la tabla está vacía y no contiene elementos, no se imprime ningún empty <ul></ul> se imprime.

Si especificamos la condición en la etiqueta de apertura {iterateWhile}, el comportamiento cambia: la condición (y la transición al siguiente elemento) se realiza al principio del ciclo interno, no al final. Así, mientras que siempre se entra en {iterateWhile} sin condiciones, sólo se entra en {iterateWhile $cond} cuando se cumple la condición $cond. Y al mismo tiempo, el siguiente elemento se escribe en $item.

Esto es útil, por ejemplo, en una situación en la que queremos renderizar el primer elemento de cada categoría de forma diferente, así:

<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 rendericemos el primer elemento y luego en el ciclo interno {iterateWhile} rendericemos 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 ciclo, podemos crear varios bucles internos e incluso anidarlos. De esta forma, se podrían agrupar subcategorías, por ejemplo.

Supongamos que la tabla tiene otra columna subcategoryId, y además de que cada categoría está en una separada <ul>cada subcategoría en un <ol>:

{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