Tudo o que você sempre quis saber sobre agrupamento
Ao trabalhar com dados em templates, você pode frequentemente encontrar a necessidade de agrupá-los ou exibi-los especificamente de acordo com certos critérios. Latte oferece várias ferramentas poderosas para este propósito.
O filtro e a função |group
permitem agrupar dados eficientemente de acordo com um critério especificado,
o filtro |batch
facilita a divisão de dados em lotes fixos, e a tag {iterateWhile}
fornece a
capacidade de controlar de forma mais complexa o fluxo de loops com condições. Cada uma dessas tags oferece possibilidades
específicas para trabalhar com dados, tornando-as ferramentas indispensáveis para a exibição dinâmica e estruturada de
informações nos templates Latte.
Filtro e função group
Imagine uma tabela de banco de dados items
com itens divididos em categorias:
id | categoryId | name |
---|---|---|
1 | 1 | Apple |
2 | 1 | Banana |
3 | 2 | PHP |
4 | 3 | Green |
5 | 3 | Red |
6 | 3 | Blue |
Uma lista simples de todos os itens usando um template Latte seria assim:
<ul>
{foreach $items as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
No entanto, se quiséssemos que os itens fossem organizados em grupos por categoria, precisaríamos dividi-los de forma que cada categoria tivesse sua própria lista. O resultado deveria então ser o seguinte:
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
<ul>
<li>PHP</li>
</ul>
<ul>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
A tarefa pode ser resolvida fácil e elegantemente usando |group
. Como parâmetro, especificamos
categoryId
, o que significa que os itens serão divididos em arrays menores com base no valor de
$item->categoryId
(se $item
fosse um array, seria usado $item['categoryId']
):
{foreach ($items|group: categoryId) as $categoryId => $categoryItems}
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
O filtro também pode ser usado em Latte como uma função, o que nos dá uma sintaxe alternativa:
{foreach group($items, categoryId) ...}
.
Se você quiser agrupar itens com base em critérios mais complexos, pode usar uma função no parâmetro do filtro. Por exemplo, agrupar itens pelo comprimento do nome seria assim:
{foreach ($items|group: fn($item) => strlen($item->name)) as $items}
...
{/foreach}
É importante notar que $categoryItems
não é um array comum, mas um objeto que se comporta como um iterador.
Para acessar o primeiro item do grupo, você pode usar a função first()
.
Essa flexibilidade no agrupamento de dados torna group
uma ferramenta excepcionalmente útil para apresentar dados
nos templates Latte.
Loops aninhados
Imagine que temos uma tabela de banco de dados com outra coluna subcategoryId
, que define das subcategorias dos
itens individuais. Queremos exibir cada categoria principal em uma lista <ul>
separada e cada subcategoria em
uma lista aninhada <ol>
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}
Conexão com Nette Database
Vamos mostrar como usar eficientemente o agrupamento de dados em combinação com a Nette Database. Suponha que estamos
trabalhando com a tabela items
do exemplo introdutório, que está conectada através da coluna
categoryId
a esta tabela categories
:
categoryId | name |
---|---|
1 | Fruits |
2 | Languages |
3 | Colors |
Carregamos os dados da tabela items
usando o Nette Database Explorer com o comando
$items = $db->table('items')
. Durante a iteração sobre esses dados, temos a possibilidade de acessar não apenas
atributos como $item->name
e $item->categoryId
, mas também, graças à conexão com a tabela
categories
, a linha relacionada nela através de $item->category
. Nesta conexão, podemos demonstrar
um uso interessante:
{foreach ($items|group: category) as $category => $categoryItems}
<h1>{$category->name}</h1>
<ul>
{foreach $categoryItems as $item}
<li>{$item->name}</li>
{/foreach}
</ul>
{/foreach}
Neste caso, usamos o filtro |group
para agrupar pela linha conectada $item->category
, e não
apenas pela coluna categoryId
. Graças a isso, na variável chave, temos diretamente o ActiveRow
da
categoria dada, o que nos permite exibir diretamente seu nome usando {$category->name}
. Este é um exemplo
prático de como o agrupamento pode tornar os templates mais claros e facilitar o trabalho com dados.
Filtro |batch
O filtro permite dividir uma lista de elementos em grupos com um número predeterminado de elementos. Este filtro é ideal para situações em que você deseja apresentar dados em vários grupos menores, por exemplo, para melhor clareza ou organização visual na página.
Imagine que temos uma lista de itens e queremos exibi-los em listas, onde cada uma contém no máximo três itens. O uso do
filtro |batch
é muito prático nesse caso:
<ul>
{foreach ($items|batch: 3) as $batch}
{foreach $batch as $item}
<li>{$item->name}</li>
{/foreach}
{/foreach}
</ul>
Neste exemplo, a lista $items
é dividida em grupos menores, onde cada grupo ($batch
) contém até
três itens. Cada grupo é então exibido em uma lista <ul>
separada.
Se o último grupo não contiver elementos suficientes para atingir o número desejado, o segundo parâmetro do filtro permite definir com o que este grupo será preenchido. Isso é ideal para o alinhamento estético de elementos onde uma linha incompleta poderia parecer desorganizada.
{foreach ($items|batch: 3, '—') as $batch}
...
{/foreach}
Tag {iterateWhile}
As mesmas tarefas que resolvemos com o filtro |group
, mostraremos usando a tag {iterateWhile}
.
A principal diferença entre as duas abordagens é que group
primeiro processa e agrupa todos os dados de entrada,
enquanto {iterateWhile}
controla o fluxo dos loops com condições, de modo que a iteração ocorre
progressivamente.
Primeiro, renderizamos a tabela com categorias usando iterateWhile:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}</li>
{/iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
</ul>
{/foreach}
Enquanto {foreach}
marca a parte externa do loop, ou seja, a renderização das listas para cada categoria, a tag
{iterateWhile}
marca a parte interna, ou seja, os itens individuais. A condição na tag de fechamento diz que a
repetição continuará enquanto o elemento atual e o seguinte pertencerem à mesma categoria
($iterator->nextValue
é o próximo item).
Se a condição fosse sempre verdadeira, todos os elementos seriam renderizados no loop interno:
{foreach $items as $item}
<ul>
{iterateWhile}
<li>{$item->name}
{/iterateWhile true}
</ul>
{/foreach}
O resultado seria assim:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>PHP</li>
<li>Green</li>
<li>Red</li>
<li>Blue</li>
</ul>
Para que serve tal uso de iterateWhile? Se a tabela estiver vazia e não contiver nenhum elemento, o
<ul></ul>
vazio não será impresso.
Se especificarmos a condição na tag de abertura {iterateWhile}
, o comportamento muda: a condição (e a
transição para o próximo elemento) é executada já no início do loop interno, e não no final. Ou seja, enquanto se entra
sempre em {iterateWhile}
sem condição, entra-se em {iterateWhile $cond}
apenas se a condição
$cond
for atendida. E, ao mesmo tempo, o próximo elemento é atribuído a $item
.
Isso é útil, por exemplo, na situação em que queremos renderizar o primeiro elemento de cada categoria de forma diferente, por exemplo, assim:
<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 o código original para que primeiro renderizemos o primeiro item e depois, no loop interno
{iterateWhile}
, renderizemos os outros itens da mesma categoria:
{foreach $items as $item}
<h1>{$item->name}</h1>
<ul>
{iterateWhile $item->categoryId === $iterator->nextValue->categoryId}
<li>{$item->name}</li>
{/iterateWhile}
</ul>
{/foreach}
Dentro de um único loop, podemos criar vários loops internos e até mesmo aninhá-los. Desta forma, poderíamos agrupar subcategorias, etc.
Digamos que na tabela haja outra coluna subcategoryId
e, além de cada categoria estar em um
<ul>
separado, cada subcategoria estará em um <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}