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}